reth_node_builder/launch/
debug.rs

1use super::LaunchNode;
2use crate::{rpc::RethRpcAddOns, EngineNodeLauncher, Node, NodeHandle};
3use alloy_provider::network::AnyNetwork;
4use jsonrpsee::core::{DeserializeOwned, Serialize};
5use reth_chainspec::EthChainSpec;
6use reth_consensus_debug_client::{DebugConsensusClient, EtherscanBlockProvider, RpcBlockProvider};
7use reth_engine_local::LocalMiner;
8use reth_node_api::{BlockTy, FullNodeComponents, PayloadAttributesBuilder, PayloadTypes};
9use std::sync::Arc;
10use tracing::info;
11
12/// [`Node`] extension with support for debugging utilities.
13///
14/// This trait provides additional necessary conversion from RPC block type to the node's
15/// primitive block type, e.g. `alloy_rpc_types_eth::Block` to the node's internal block
16/// representation.
17///
18/// This is used in conjunction with the [`DebugNodeLauncher`] to enable debugging features such as:
19///
20/// - **Etherscan Integration**: Use Etherscan as a consensus client to follow the chain and submit
21///   blocks to the local engine.
22/// - **RPC Consensus Client**: Connect to an external RPC endpoint to fetch blocks and submit them
23///   to the local engine to follow the chain.
24///
25/// See [`DebugNodeLauncher`] for the launcher that enables these features.
26///
27/// # Implementation
28///
29/// To implement this trait, you need to:
30/// 1. Define the RPC block type (typically `alloy_rpc_types_eth::Block`)
31/// 2. Implement the conversion from RPC format to your primitive block type
32///
33/// # Example
34///
35/// ```ignore
36/// impl<N: FullNodeComponents<Types = Self>> DebugNode<N> for MyNode {
37///     type RpcBlock = alloy_rpc_types_eth::Block;
38///
39///     fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> BlockTy<Self> {
40///         // Convert from RPC format to primitive format by converting the transactions
41///         rpc_block.into_consensus().convert_transactions()
42///     }
43/// }
44/// ```
45pub trait DebugNode<N: FullNodeComponents>: Node<N> {
46    /// RPC block type. Used by [`DebugConsensusClient`] to fetch blocks and submit them to the
47    /// engine. This is intended to match the block format returned by the external RPC endpoint.
48    type RpcBlock: Serialize + DeserializeOwned + 'static;
49
50    /// Converts an RPC block to a primitive block.
51    ///
52    /// This method handles the conversion between the RPC block format and the internal primitive
53    /// block format used by the node's consensus engine.
54    ///
55    /// # Example
56    ///
57    /// For Ethereum nodes, this typically converts from `alloy_rpc_types_eth::Block`
58    /// to the node's internal block representation.
59    fn rpc_to_primitive_block(rpc_block: Self::RpcBlock) -> BlockTy<Self>;
60
61    /// Creates a payload attributes builder for local mining in dev mode.
62    ///
63    ///  It will be used by the `LocalMiner` when dev mode is enabled.
64    ///
65    /// The builder is responsible for creating the payload attributes that define how blocks should
66    /// be constructed during local mining.
67    fn local_payload_attributes_builder(
68        chain_spec: &Self::ChainSpec,
69    ) -> impl PayloadAttributesBuilder<
70        <<Self as reth_node_api::NodeTypes>::Payload as PayloadTypes>::PayloadAttributes,
71    >;
72}
73
74/// Node launcher with support for launching various debugging utilities.
75///
76/// This launcher wraps an existing launcher and adds debugging capabilities when
77/// certain debug flags are enabled. It provides two main debugging features:
78///
79/// ## RPC Consensus Client
80///
81/// When `--debug.rpc-consensus-ws <URL>` is provided, the launcher will:
82/// - Connect to an external RPC `WebSocket` endpoint
83/// - Fetch blocks from that endpoint
84/// - Submit them to the local engine for execution
85/// - Useful for testing engine behavior with real network data
86///
87/// ## Etherscan Consensus Client
88///
89/// When `--debug.etherscan [URL]` is provided, the launcher will:
90/// - Use Etherscan API as a consensus client
91/// - Fetch recent blocks from Etherscan
92/// - Submit them to the local engine
93/// - Requires `ETHERSCAN_API_KEY` environment variable
94/// - Falls back to default Etherscan URL for the chain if URL not provided
95#[derive(Debug, Clone)]
96pub struct DebugNodeLauncher<L = EngineNodeLauncher> {
97    inner: L,
98}
99
100impl<L> DebugNodeLauncher<L> {
101    /// Creates a new instance of the [`DebugNodeLauncher`].
102    pub const fn new(inner: L) -> Self {
103        Self { inner }
104    }
105}
106
107impl<L, Target, N, AddOns> LaunchNode<Target> for DebugNodeLauncher<L>
108where
109    N: FullNodeComponents<Types: DebugNode<N>>,
110    AddOns: RethRpcAddOns<N>,
111    L: LaunchNode<Target, Node = NodeHandle<N, AddOns>>,
112{
113    type Node = NodeHandle<N, AddOns>;
114
115    async fn launch_node(self, target: Target) -> eyre::Result<Self::Node> {
116        let handle = self.inner.launch_node(target).await?;
117
118        let config = &handle.node.config;
119        if let Some(ws_url) = config.debug.rpc_consensus_ws.clone() {
120            info!(target: "reth::cli", "Using RPC WebSocket consensus client: {}", ws_url);
121
122            let block_provider =
123                RpcBlockProvider::<AnyNetwork, _>::new(ws_url.as_str(), |block_response| {
124                    let json = serde_json::to_value(block_response)
125                        .expect("Block serialization cannot fail");
126                    let rpc_block =
127                        serde_json::from_value(json).expect("Block deserialization cannot fail");
128                    N::Types::rpc_to_primitive_block(rpc_block)
129                })
130                .await?;
131
132            let rpc_consensus_client = DebugConsensusClient::new(
133                handle.node.add_ons_handle.beacon_engine_handle.clone(),
134                Arc::new(block_provider),
135            );
136
137            handle.node.task_executor.spawn_critical("rpc-ws consensus client", async move {
138                rpc_consensus_client.run().await
139            });
140        }
141
142        if let Some(maybe_custom_etherscan_url) = config.debug.etherscan.clone() {
143            info!(target: "reth::cli", "Using etherscan as consensus client");
144
145            let chain = config.chain.chain();
146            let etherscan_url = maybe_custom_etherscan_url.map(Ok).unwrap_or_else(|| {
147                // If URL isn't provided, use default Etherscan URL for the chain if it is known
148                chain
149                    .etherscan_urls()
150                    .map(|urls| urls.0.to_string())
151                    .ok_or_else(|| eyre::eyre!("failed to get etherscan url for chain: {chain}"))
152            })?;
153
154            let block_provider = EtherscanBlockProvider::new(
155                etherscan_url,
156                chain.etherscan_api_key().ok_or_else(|| {
157                    eyre::eyre!(
158                        "etherscan api key not found for rpc consensus client for chain: {chain}"
159                    )
160                })?,
161                chain.id(),
162                N::Types::rpc_to_primitive_block,
163            );
164            let rpc_consensus_client = DebugConsensusClient::new(
165                handle.node.add_ons_handle.beacon_engine_handle.clone(),
166                Arc::new(block_provider),
167            );
168            handle.node.task_executor.spawn_critical("etherscan consensus client", async move {
169                rpc_consensus_client.run().await
170            });
171        }
172
173        if config.dev.dev {
174            info!(target: "reth::cli", "Using local payload attributes builder for dev mode");
175
176            let blockchain_db = handle.node.provider.clone();
177            let chain_spec = config.chain.clone();
178            let beacon_engine_handle = handle.node.add_ons_handle.beacon_engine_handle.clone();
179            let pool = handle.node.pool.clone();
180            let payload_builder_handle = handle.node.payload_builder_handle.clone();
181
182            let dev_mining_mode = handle.node.config.dev_mining_mode(pool);
183            handle.node.task_executor.spawn_critical("local engine", async move {
184                LocalMiner::new(
185                    blockchain_db,
186                    N::Types::local_payload_attributes_builder(&chain_spec),
187                    beacon_engine_handle,
188                    dev_mining_mode,
189                    payload_builder_handle,
190                )
191                .run()
192                .await
193            });
194        }
195
196        Ok(handle)
197    }
198}