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