Skip to main content

reth_node_builder/launch/
debug.rs

1use super::LaunchNode;
2use crate::{rpc::RethRpcAddOns, EngineNodeLauncher, Node, NodeHandle};
3use alloy_consensus::transaction::Either;
4use alloy_provider::network::AnyNetwork;
5use jsonrpsee::core::{DeserializeOwned, Serialize};
6use reth_chainspec::EthChainSpec;
7use reth_consensus_debug_client::{
8    BlockProvider, DebugConsensusClient, EtherscanBlockProvider, RpcBlockProvider,
9};
10use reth_engine_local::{LocalMiner, MiningMode};
11use reth_node_api::{
12    BlockTy, FullNodeComponents, FullNodeTypes, HeaderTy, PayloadAttrTy, PayloadAttributesBuilder,
13    PayloadTypes,
14};
15use std::{
16    future::{Future, IntoFuture},
17    pin::Pin,
18    sync::Arc,
19};
20use tracing::info;
21
22/// [`Node`] extension with support for debugging utilities.
23///
24/// This trait provides additional necessary conversion from RPC block type to the node's
25/// primitive block type, e.g. `alloy_rpc_types_eth::Block` to the node's internal block
26/// representation.
27///
28/// This is used in conjunction with the [`DebugNodeLauncher`] to enable debugging features such as:
29///
30/// - **Etherscan Integration**: Use Etherscan as a consensus client to follow the chain and submit
31///   blocks to the local engine.
32/// - **RPC Consensus Client**: Connect to an external RPC endpoint to fetch blocks and submit them
33///   to the local engine to follow the chain.
34///
35/// See [`DebugNodeLauncher`] for the launcher that enables these features.
36///
37/// # Implementation
38///
39/// To implement this trait, you need to:
40/// 1. Define the RPC block type (typically `alloy_rpc_types_eth::Block`)
41/// 2. Implement the conversion from RPC format to your primitive block type
42///
43/// # Example
44///
45/// ```ignore
46/// impl<N: FullNodeComponents<Types = Self>> DebugNode<N> for MyNode {
47///     type RpcBlock = alloy_rpc_types_eth::Block;
48///
49///     fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> BlockTy<Self> {
50///         // Convert from RPC format to primitive format by converting the transactions
51///         rpc_block.into_consensus().convert_transactions()
52///     }
53/// }
54/// ```
55pub trait DebugNode<N: FullNodeComponents>: Node<N> {
56    /// RPC block type. Used by [`DebugConsensusClient`] to fetch blocks and submit them to the
57    /// engine. This is intended to match the block format returned by the external RPC endpoint.
58    type RpcBlock: Serialize + DeserializeOwned + 'static;
59
60    /// Converts an RPC block to a primitive block.
61    ///
62    /// This method handles the conversion between the RPC block format and the internal primitive
63    /// block format used by the node's consensus engine.
64    ///
65    /// # Example
66    ///
67    /// For Ethereum nodes, this typically converts from `alloy_rpc_types_eth::Block`
68    /// to the node's internal block representation.
69    fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> BlockTy<Self>;
70
71    /// Creates a payload attributes builder for local mining in dev mode.
72    ///
73    ///  It will be used by the `LocalMiner` when dev mode is enabled.
74    ///
75    /// The builder is responsible for creating the payload attributes that define how blocks should
76    /// be constructed during local mining.
77    fn local_payload_attributes_builder(
78        chain_spec: &Self::ChainSpec,
79    ) -> impl PayloadAttributesBuilder<<Self::Payload as PayloadTypes>::PayloadAttributes, HeaderTy<Self>>;
80}
81
82/// Node launcher with support for launching various debugging utilities.
83///
84/// This launcher wraps an existing launcher and adds debugging capabilities when
85/// certain debug flags are enabled. It provides two main debugging features:
86///
87/// ## RPC Consensus Client
88///
89/// When `--debug.rpc-consensus-ws <URL>` is provided, the launcher will:
90/// - Connect to an external RPC endpoint (`WebSocket` or HTTP)
91/// - Fetch blocks from that endpoint (using subscriptions for `WebSocket`, polling for HTTP)
92/// - Submit them to the local engine for execution
93/// - Useful for testing engine behavior with real network data
94///
95/// ## Etherscan Consensus Client
96///
97/// When `--debug.etherscan [URL]` is provided, the launcher will:
98/// - Use Etherscan API as a consensus client
99/// - Fetch recent blocks from Etherscan
100/// - Submit them to the local engine
101/// - Requires `ETHERSCAN_API_KEY` environment variable
102/// - Falls back to default Etherscan URL for the chain if URL not provided
103#[derive(Debug, Clone)]
104pub struct DebugNodeLauncher<L = EngineNodeLauncher> {
105    inner: L,
106}
107
108impl<L> DebugNodeLauncher<L> {
109    /// Creates a new instance of the [`DebugNodeLauncher`].
110    pub const fn new(inner: L) -> Self {
111        Self { inner }
112    }
113}
114
115/// Type alias for the default debug block provider. We use etherscan provider to satisfy the
116/// bounds.
117pub type DefaultDebugBlockProvider<N> = EtherscanBlockProvider<
118    <<N as FullNodeTypes>::Types as DebugNode<N>>::RpcBlock,
119    BlockTy<<N as FullNodeTypes>::Types>,
120>;
121
122/// Future for the [`DebugNodeLauncher`].
123#[expect(missing_debug_implementations, clippy::type_complexity)]
124pub struct DebugNodeLauncherFuture<L, Target, N, B = DefaultDebugBlockProvider<N>>
125where
126    N: FullNodeComponents<Types: DebugNode<N>>,
127{
128    inner: L,
129    target: Target,
130    local_payload_attributes_builder:
131        Option<Box<dyn PayloadAttributesBuilder<PayloadAttrTy<N::Types>, HeaderTy<N::Types>>>>,
132    map_attributes:
133        Option<Box<dyn Fn(PayloadAttrTy<N::Types>) -> PayloadAttrTy<N::Types> + Send + Sync>>,
134    debug_block_provider: Option<B>,
135    mining_mode: Option<MiningMode<N::Pool>>,
136}
137
138impl<L, Target, N, AddOns, B> DebugNodeLauncherFuture<L, Target, N, B>
139where
140    N: FullNodeComponents<Types: DebugNode<N>>,
141    AddOns: RethRpcAddOns<N>,
142    L: LaunchNode<Target, Node = NodeHandle<N, AddOns>>,
143    B: BlockProvider<Block = BlockTy<N::Types>> + Clone,
144{
145    /// Sets a custom payload attributes builder for local mining in dev mode.
146    pub fn with_payload_attributes_builder(
147        self,
148        builder: impl PayloadAttributesBuilder<PayloadAttrTy<N::Types>, HeaderTy<N::Types>>,
149    ) -> Self {
150        Self {
151            inner: self.inner,
152            target: self.target,
153            local_payload_attributes_builder: Some(Box::new(builder)),
154            map_attributes: None,
155            debug_block_provider: self.debug_block_provider,
156            mining_mode: self.mining_mode,
157        }
158    }
159
160    /// Sets a function to map payload attributes before building.
161    pub fn map_debug_payload_attributes(
162        self,
163        f: impl Fn(PayloadAttrTy<N::Types>) -> PayloadAttrTy<N::Types> + Send + Sync + 'static,
164    ) -> Self {
165        Self {
166            inner: self.inner,
167            target: self.target,
168            local_payload_attributes_builder: None,
169            map_attributes: Some(Box::new(f)),
170            debug_block_provider: self.debug_block_provider,
171            mining_mode: self.mining_mode,
172        }
173    }
174
175    /// Sets a custom [`MiningMode`] for the local miner in dev mode.
176    ///
177    /// This overrides the default mining mode that is derived from the node configuration
178    /// (instant or interval). This can be used to provide a custom trigger-based mining mode.
179    pub fn with_mining_mode(mut self, mode: MiningMode<N::Pool>) -> Self {
180        self.mining_mode = Some(mode);
181        self
182    }
183
184    /// Sets a custom block provider for the debug consensus client.
185    ///
186    /// When set, this provider will be used instead of creating an `EtherscanBlockProvider`
187    /// or `RpcBlockProvider` from CLI arguments.
188    pub fn with_debug_block_provider<B2>(
189        self,
190        provider: B2,
191    ) -> DebugNodeLauncherFuture<L, Target, N, B2>
192    where
193        B2: BlockProvider<Block = BlockTy<N::Types>> + Clone,
194    {
195        DebugNodeLauncherFuture {
196            inner: self.inner,
197            target: self.target,
198            local_payload_attributes_builder: self.local_payload_attributes_builder,
199            map_attributes: self.map_attributes,
200            debug_block_provider: Some(provider),
201            mining_mode: self.mining_mode,
202        }
203    }
204
205    async fn launch_node(self) -> eyre::Result<NodeHandle<N, AddOns>> {
206        let Self {
207            inner,
208            target,
209            local_payload_attributes_builder,
210            map_attributes,
211            debug_block_provider,
212            mining_mode,
213        } = self;
214
215        let handle = inner.launch_node(target).await?;
216
217        let config = &handle.node.config;
218
219        if let Some(provider) = debug_block_provider {
220            info!(target: "reth::cli", "Using custom debug block provider");
221
222            let rpc_consensus_client = DebugConsensusClient::new(
223                handle.node.add_ons_handle.beacon_engine_handle.clone(),
224                Arc::new(provider),
225            );
226
227            handle
228                .node
229                .task_executor
230                .spawn_critical_task("custom debug block provider consensus client", async move {
231                    rpc_consensus_client.run().await
232                });
233        } else if let Some(url) = config.debug.rpc_consensus_url.clone() {
234            info!(target: "reth::cli", "Using RPC consensus client: {}", url);
235
236            let block_provider =
237                RpcBlockProvider::<AnyNetwork, _>::new(url.as_str(), |block_response| {
238                    let json = serde_json::to_value(block_response)
239                        .expect("Block serialization cannot fail");
240                    let rpc_block =
241                        serde_json::from_value(json).expect("Block deserialization cannot fail");
242                    N::Types::rpc_to_primitive_block(rpc_block)
243                })
244                .await?;
245
246            let rpc_consensus_client = DebugConsensusClient::new(
247                handle.node.add_ons_handle.beacon_engine_handle.clone(),
248                Arc::new(block_provider),
249            );
250
251            handle.node.task_executor.spawn_critical_task("rpc-ws consensus client", async move {
252                rpc_consensus_client.run().await
253            });
254        } else if let Some(maybe_custom_etherscan_url) = config.debug.etherscan.clone() {
255            info!(target: "reth::cli", "Using etherscan as consensus client");
256
257            let chain = config.chain.chain();
258            let etherscan_url = maybe_custom_etherscan_url.map(Ok).unwrap_or_else(|| {
259                chain
260                    .etherscan_urls()
261                    .map(|urls| urls.0.to_string())
262                    .ok_or_else(|| eyre::eyre!("failed to get etherscan url for chain: {chain}"))
263            })?;
264
265            let block_provider = EtherscanBlockProvider::new(
266                etherscan_url,
267                chain.etherscan_api_key().ok_or_else(|| {
268                    eyre::eyre!(
269                        "etherscan api key not found for rpc consensus client for chain: {chain}"
270                    )
271                })?,
272                chain.id(),
273                N::Types::rpc_to_primitive_block,
274            );
275            let rpc_consensus_client = DebugConsensusClient::new(
276                handle.node.add_ons_handle.beacon_engine_handle.clone(),
277                Arc::new(block_provider),
278            );
279            handle
280                .node
281                .task_executor
282                .spawn_critical_task("etherscan consensus client", async move {
283                    rpc_consensus_client.run().await
284                });
285        }
286
287        if config.dev.dev {
288            info!(target: "reth::cli", "Using local payload attributes builder for dev mode");
289
290            let blockchain_db = handle.node.provider.clone();
291            let chain_spec = config.chain.clone();
292            let beacon_engine_handle = handle.node.add_ons_handle.beacon_engine_handle.clone();
293            let pool = handle.node.pool.clone();
294            let payload_builder_handle = handle.node.payload_builder_handle.clone();
295
296            let builder = if let Some(builder) = local_payload_attributes_builder {
297                Either::Left(builder)
298            } else {
299                let local = N::Types::local_payload_attributes_builder(&chain_spec);
300                let builder = if let Some(f) = map_attributes {
301                    Either::Left(move |parent| f(local.build(&parent)))
302                } else {
303                    Either::Right(local)
304                };
305                Either::Right(builder)
306            };
307
308            let dev_mining_mode =
309                mining_mode.unwrap_or_else(|| handle.node.config.dev_mining_mode(pool));
310            handle.node.task_executor.spawn_critical_task("local engine", async move {
311                LocalMiner::new(
312                    blockchain_db,
313                    builder,
314                    beacon_engine_handle,
315                    dev_mining_mode,
316                    payload_builder_handle,
317                )
318                .run()
319                .await
320            });
321        }
322
323        Ok(handle)
324    }
325}
326
327impl<L, Target, N, AddOns, B> IntoFuture for DebugNodeLauncherFuture<L, Target, N, B>
328where
329    Target: Send + 'static,
330    N: FullNodeComponents<Types: DebugNode<N>>,
331    AddOns: RethRpcAddOns<N> + 'static,
332    L: LaunchNode<Target, Node = NodeHandle<N, AddOns>> + 'static,
333    B: BlockProvider<Block = BlockTy<N::Types>> + Clone + 'static,
334{
335    type Output = eyre::Result<NodeHandle<N, AddOns>>;
336    type IntoFuture = Pin<Box<dyn Future<Output = eyre::Result<NodeHandle<N, AddOns>>> + Send>>;
337
338    fn into_future(self) -> Self::IntoFuture {
339        Box::pin(self.launch_node())
340    }
341}
342
343impl<L, Target, N, AddOns> LaunchNode<Target> for DebugNodeLauncher<L>
344where
345    Target: Send + 'static,
346    N: FullNodeComponents<Types: DebugNode<N>>,
347    AddOns: RethRpcAddOns<N> + 'static,
348    L: LaunchNode<Target, Node = NodeHandle<N, AddOns>> + 'static,
349    DefaultDebugBlockProvider<N>: BlockProvider<Block = BlockTy<N::Types>> + Clone,
350{
351    type Node = NodeHandle<N, AddOns>;
352    type Future = DebugNodeLauncherFuture<L, Target, N>;
353
354    fn launch_node(self, target: Target) -> Self::Future {
355        DebugNodeLauncherFuture {
356            inner: self.inner,
357            target,
358            local_payload_attributes_builder: None,
359            map_attributes: None,
360            debug_block_provider: None,
361            mining_mode: None,
362        }
363    }
364}