Skip to main content

reth_cli_commands/p2p/
mod.rs

1//! P2P Debugging tool
2
3use std::{path::PathBuf, sync::Arc};
4
5use crate::common::CliNodeTypes;
6use alloy_eips::BlockHashOrNumber;
7use backon::{ConstantBuilder, Retryable};
8use clap::{Parser, Subcommand};
9use eyre::WrapErr;
10use futures::FutureExt;
11use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks};
12use reth_cli::chainspec::ChainSpecParser;
13use reth_cli_util::hash_or_num_value_parser;
14use reth_config::Config;
15use reth_network::{BlockDownloaderProvider, NetworkConfigBuilder};
16use reth_network_p2p::bodies::client::BodiesClient;
17use reth_node_core::{
18    args::{DatadirArgs, NetworkArgs},
19    utils::get_single_header,
20};
21use reth_primitives_traits::BlockBody;
22use reth_tasks::Runtime;
23
24pub mod bootnode;
25pub mod enode;
26pub mod rlpx;
27
28/// `reth p2p` command
29#[derive(Debug, Parser)]
30pub struct Command<C: ChainSpecParser> {
31    #[command(subcommand)]
32    command: Subcommands<C>,
33}
34
35impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
36    /// Execute `p2p` command
37    pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>>(self) -> eyre::Result<()> {
38        match self.command {
39            Subcommands::Header { args, id } => {
40                let handle = args.launch_network::<N>().await?;
41                let fetch_client = handle.fetch_client().await?;
42                let backoff = args.backoff();
43
44                let header = (move || get_single_header(fetch_client.clone(), id))
45                    .retry(backoff)
46                    .notify(|err, _| tracing::warn!(target: "reth::cli", error = %err, "Error requesting header. Retrying..."))
47                    .await?;
48                tracing::info!(target: "reth::cli", ?header, "Successfully downloaded header");
49            }
50
51            Subcommands::Body { args, id } => {
52                let handle = args.launch_network::<N>().await?;
53                let fetch_client = handle.fetch_client().await?;
54                let backoff = args.backoff();
55
56                let hash = match id {
57                    BlockHashOrNumber::Hash(hash) => hash,
58                    BlockHashOrNumber::Number(number) => {
59                        tracing::info!(target: "reth::cli", "Block number provided. Downloading header first...");
60                        let client = fetch_client.clone();
61                        let header = (move || {
62                            get_single_header(client.clone(), BlockHashOrNumber::Number(number))
63                        })
64                        .retry(backoff)
65                        .notify(|err, _| tracing::warn!(target: "reth::cli", error = %err, "Error requesting header. Retrying..."))
66                        .await?;
67                        header.hash()
68                    }
69                };
70                let (_, result) = (move || {
71                    let client = fetch_client.clone();
72                    client.get_block_bodies(vec![hash])
73                })
74                .retry(backoff)
75                .notify(|err, _| tracing::warn!(target: "reth::cli", error = %err, "Error requesting block. Retrying..."))
76                .await?
77                .split();
78                if result.len() != 1 {
79                    eyre::bail!(
80                        "Invalid number of bodies received. Expected: 1. Received: {}",
81                        result.len()
82                    );
83                }
84                let body = result.into_iter().next().unwrap();
85                tracing::info!(target: "reth::cli", %hash, txs = body.transaction_count(), "Successfully downloaded body")
86            }
87            Subcommands::Rlpx(command) => {
88                command.execute().await?;
89            }
90            Subcommands::Bootnode(command) => {
91                command.execute().await?;
92            }
93            Subcommands::Enode(command) => {
94                command.execute()?;
95            }
96        }
97
98        Ok(())
99    }
100}
101
102impl<C: ChainSpecParser> Command<C> {
103    /// Returns the underlying chain being used to run this command
104    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
105        match &self.command {
106            Subcommands::Header { args, .. } => Some(&args.chain),
107            Subcommands::Body { args, .. } => Some(&args.chain),
108            Subcommands::Rlpx(_) => None,
109            Subcommands::Bootnode(_) => None,
110            Subcommands::Enode(_) => None,
111        }
112    }
113}
114
115/// `reth p2p` subcommands
116#[derive(Subcommand, Debug)]
117pub enum Subcommands<C: ChainSpecParser> {
118    /// Download block header
119    Header {
120        #[command(flatten)]
121        args: DownloadArgs<C>,
122        /// The header number or hash
123        #[arg(value_parser = hash_or_num_value_parser)]
124        id: BlockHashOrNumber,
125    },
126    /// Download block body
127    Body {
128        #[command(flatten)]
129        args: DownloadArgs<C>,
130        /// The block number or hash
131        #[arg(value_parser = hash_or_num_value_parser)]
132        id: BlockHashOrNumber,
133    },
134    // RLPx utilities
135    Rlpx(rlpx::Command),
136    /// Bootnode command
137    Bootnode(bootnode::Command),
138    /// Print enode identifier
139    Enode(enode::Command),
140}
141
142#[derive(Debug, Clone, Parser)]
143pub struct DownloadArgs<C: ChainSpecParser> {
144    /// The number of retries per request
145    #[arg(long, default_value = "5")]
146    retries: usize,
147
148    #[command(flatten)]
149    network: NetworkArgs,
150
151    #[command(flatten)]
152    datadir: DatadirArgs,
153
154    /// The path to the configuration file to use.
155    #[arg(long, value_name = "FILE", verbatim_doc_comment)]
156    config: Option<PathBuf>,
157
158    /// The chain this node is running.
159    ///
160    /// Possible values are either a built-in chain or the path to a chain specification file.
161    #[arg(
162        long,
163        value_name = "CHAIN_OR_PATH",
164        long_help = C::help_message(),
165        default_value = C::default_value(),
166        value_parser = C::parser()
167    )]
168    chain: Arc<C::ChainSpec>,
169}
170
171impl<C: ChainSpecParser> DownloadArgs<C> {
172    /// Creates and spawns the network and returns the handle.
173    pub async fn launch_network<N>(
174        &self,
175    ) -> eyre::Result<reth_network::NetworkHandle<N::NetworkPrimitives>>
176    where
177        C::ChainSpec: EthChainSpec + Hardforks + EthereumHardforks + 'static,
178        N: CliNodeTypes<ChainSpec = C::ChainSpec>,
179    {
180        let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
181        let config_path = self.config.clone().unwrap_or_else(|| data_dir.config());
182
183        // Load configuration
184        let mut config = Config::from_path(&config_path)
185            .wrap_err_with(|| format!("Could not load config file {config_path:?}"))?;
186
187        config.peers.trusted_nodes.extend(self.network.trusted_peers.clone());
188
189        if config.peers.trusted_nodes.is_empty() && self.network.trusted_only {
190            eyre::bail!(
191                "No trusted nodes. Set trusted peer with `--trusted-peer <enode record>` or set `--trusted-only` to `false`"
192            );
193        }
194
195        config.peers.trusted_nodes_only |= self.network.trusted_only;
196
197        let default_secret_key_path = data_dir.p2p_secret();
198        let p2p_secret_key = self.network.secret_key(default_secret_key_path)?;
199        let rlpx_socket = (self.network.addr, self.network.port).into();
200        let boot_nodes = self
201            .network
202            .resolved_bootnodes()
203            .unwrap_or_else(|| self.chain.bootnodes().unwrap_or_default());
204
205        let net =
206            NetworkConfigBuilder::<N::NetworkPrimitives>::new(p2p_secret_key, Runtime::test())
207                .peer_config(config.peers_config_with_basic_nodes_from_file(None))
208                .sessions_config(config.sessions)
209                .external_ip_resolver(self.network.nat.clone())
210                .network_id(self.network.network_id)
211                .boot_nodes(boot_nodes.clone())
212                .apply(|builder| {
213                    self.network.discovery.apply_to_builder(builder, rlpx_socket, boot_nodes)
214                })
215                .build_with_noop_provider(self.chain.clone())
216                .manager()
217                .await?;
218        let handle = net.handle().clone();
219        tokio::task::spawn(net.boxed());
220
221        Ok(handle)
222    }
223
224    pub fn backoff(&self) -> ConstantBuilder {
225        ConstantBuilder::default().with_max_times(self.retries.max(1))
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
233
234    #[test]
235    fn parse_header_cmd() {
236        let _args: Command<EthereumChainSpecParser> =
237            Command::parse_from(["reth", "header", "--chain", "mainnet", "1000"]);
238    }
239
240    #[test]
241    fn parse_body_cmd() {
242        let _args: Command<EthereumChainSpecParser> =
243            Command::parse_from(["reth", "body", "--chain", "mainnet", "1000"]);
244    }
245
246    #[test]
247    fn parse_enode_cmd() {
248        let _args: Command<EthereumChainSpecParser> =
249            Command::parse_from(["reth", "enode", "/tmp/secret"]);
250    }
251
252    #[test]
253    fn parse_enode_cmd_with_ip() {
254        let _args: Command<EthereumChainSpecParser> =
255            Command::parse_from(["reth", "enode", "/tmp/secret", "--ip", "192.168.1.1"]);
256    }
257}