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