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