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}