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