reth_node_core/
node_config.rs

1//! Support for customizing the node
2
3use crate::{
4    args::{
5        DatabaseArgs, DatadirArgs, DebugArgs, DevArgs, EngineArgs, NetworkArgs, PayloadBuilderArgs,
6        PruningArgs, RpcServerArgs, TxPoolArgs,
7    },
8    dirs::{ChainPath, DataDirPath},
9    utils::get_single_header,
10};
11use alloy_consensus::BlockHeader;
12use alloy_eips::BlockHashOrNumber;
13use alloy_primitives::{BlockNumber, B256};
14use eyre::eyre;
15use reth_chainspec::{ChainSpec, EthChainSpec, MAINNET};
16use reth_config::config::PruneConfig;
17use reth_ethereum_forks::Head;
18use reth_network_p2p::headers::client::HeadersClient;
19use reth_primitives_traits::SealedHeader;
20use reth_stages_types::StageId;
21use reth_storage_api::{
22    BlockHashReader, DatabaseProviderFactory, HeaderProvider, StageCheckpointReader,
23};
24use reth_storage_errors::provider::ProviderResult;
25use serde::{de::DeserializeOwned, Serialize};
26use std::{
27    fs,
28    net::SocketAddr,
29    path::{Path, PathBuf},
30    sync::Arc,
31};
32use tracing::*;
33
34pub use reth_engine_primitives::DEFAULT_MEMORY_BLOCK_BUFFER_TARGET;
35
36/// Triggers persistence when the number of canonical blocks in memory exceeds this threshold.
37pub const DEFAULT_PERSISTENCE_THRESHOLD: u64 = 2;
38
39/// Default size of cross-block cache in megabytes.
40pub const DEFAULT_CROSS_BLOCK_CACHE_SIZE_MB: u64 = 4 * 1024;
41
42/// This includes all necessary configuration to launch the node.
43/// The individual configuration options can be overwritten before launching the node.
44///
45/// # Example
46/// ```rust
47/// # use reth_node_core::{
48/// #     node_config::NodeConfig,
49/// #     args::RpcServerArgs,
50/// # };
51/// # use reth_rpc_server_types::RpcModuleSelection;
52/// # use tokio::runtime::Handle;
53///
54/// async fn t() {
55///     // create the builder
56///     let builder = NodeConfig::default();
57///
58///     // configure the rpc apis
59///     let mut rpc = RpcServerArgs::default().with_http().with_ws();
60///     rpc.http_api = Some(RpcModuleSelection::All);
61///     let builder = builder.with_rpc(rpc);
62/// }
63/// ```
64///
65/// This can also be used to launch a node with a temporary test database. This can be done with
66/// the [`NodeConfig::test`] method.
67///
68/// # Example
69/// ```rust
70/// # use reth_node_core::{
71/// #     node_config::NodeConfig,
72/// #     args::RpcServerArgs,
73/// # };
74/// # use reth_rpc_server_types::RpcModuleSelection;
75/// # use tokio::runtime::Handle;
76///
77/// async fn t() {
78///     // create the builder with a test database, using the `test` method
79///     let builder = NodeConfig::test();
80///
81///     // configure the rpc apis
82///     let mut rpc = RpcServerArgs::default().with_http().with_ws();
83///     rpc.http_api = Some(RpcModuleSelection::All);
84///     let builder = builder.with_rpc(rpc);
85/// }
86/// ```
87#[derive(Debug)]
88pub struct NodeConfig<ChainSpec> {
89    /// All data directory related arguments
90    pub datadir: DatadirArgs,
91
92    /// The path to the configuration file to use.
93    pub config: Option<PathBuf>,
94
95    /// The chain this node is running.
96    ///
97    /// Possible values are either a built-in chain or the path to a chain specification file.
98    pub chain: Arc<ChainSpec>,
99
100    /// Enable Prometheus metrics.
101    ///
102    /// The metrics will be served at the given interface and port.
103    pub metrics: Option<SocketAddr>,
104
105    /// Add a new instance of a node.
106    ///
107    /// Configures the ports of the node to avoid conflicts with the defaults.
108    /// This is useful for running multiple nodes on the same machine.
109    ///
110    /// Max number of instances is 200. It is chosen in a way so that it's not possible to have
111    /// port numbers that conflict with each other.
112    ///
113    /// Changes to the following port numbers:
114    /// - `DISCOVERY_PORT`: default + `instance` - 1
115    /// - `DISCOVERY_V5_PORT`: default + `instance` - 1
116    /// - `AUTH_PORT`: default + `instance` * 100 - 100
117    /// - `HTTP_RPC_PORT`: default - `instance` + 1
118    /// - `WS_RPC_PORT`: default + `instance` * 2 - 2
119    pub instance: u16,
120
121    /// All networking related arguments
122    pub network: NetworkArgs,
123
124    /// All rpc related arguments
125    pub rpc: RpcServerArgs,
126
127    /// All txpool related arguments with --txpool prefix
128    pub txpool: TxPoolArgs,
129
130    /// All payload builder related arguments
131    pub builder: PayloadBuilderArgs,
132
133    /// All debug related arguments with --debug prefix
134    pub debug: DebugArgs,
135
136    /// All database related arguments
137    pub db: DatabaseArgs,
138
139    /// All dev related arguments with --dev prefix
140    pub dev: DevArgs,
141
142    /// All pruning related arguments
143    pub pruning: PruningArgs,
144
145    /// All engine related arguments
146    pub engine: EngineArgs,
147}
148
149impl NodeConfig<ChainSpec> {
150    /// Creates a testing [`NodeConfig`], causing the database to be launched ephemerally.
151    pub fn test() -> Self {
152        Self::default()
153            // set all ports to zero by default for test instances
154            .with_unused_ports()
155    }
156}
157
158impl<ChainSpec> NodeConfig<ChainSpec> {
159    /// Creates a new config with given chain spec, setting all fields to default values.
160    pub fn new(chain: Arc<ChainSpec>) -> Self {
161        Self {
162            config: None,
163            chain,
164            metrics: None,
165            instance: 1,
166            network: NetworkArgs::default(),
167            rpc: RpcServerArgs::default(),
168            txpool: TxPoolArgs::default(),
169            builder: PayloadBuilderArgs::default(),
170            debug: DebugArgs::default(),
171            db: DatabaseArgs::default(),
172            dev: DevArgs::default(),
173            pruning: PruningArgs::default(),
174            datadir: DatadirArgs::default(),
175            engine: EngineArgs::default(),
176        }
177    }
178
179    /// Sets --dev mode for the node.
180    ///
181    /// In addition to setting the `--dev` flag, this also:
182    ///   - disables discovery in [`NetworkArgs`].
183    pub const fn dev(mut self) -> Self {
184        self.dev.dev = true;
185        self.network.discovery.disable_discovery = true;
186        self
187    }
188
189    /// Sets --dev mode for the node [`NodeConfig::dev`], if `dev` is true.
190    pub const fn set_dev(self, dev: bool) -> Self {
191        if dev {
192            self.dev()
193        } else {
194            self
195        }
196    }
197
198    /// Set the data directory args for the node
199    pub fn with_datadir_args(mut self, datadir_args: DatadirArgs) -> Self {
200        self.datadir = datadir_args;
201        self
202    }
203
204    /// Set the config file for the node
205    pub fn with_config(mut self, config: impl Into<PathBuf>) -> Self {
206        self.config = Some(config.into());
207        self
208    }
209
210    /// Set the [`ChainSpec`] for the node
211    pub fn with_chain(mut self, chain: impl Into<Arc<ChainSpec>>) -> Self {
212        self.chain = chain.into();
213        self
214    }
215
216    /// Set the metrics address for the node
217    pub const fn with_metrics(mut self, metrics: SocketAddr) -> Self {
218        self.metrics = Some(metrics);
219        self
220    }
221
222    /// Set the instance for the node
223    pub const fn with_instance(mut self, instance: u16) -> Self {
224        self.instance = instance;
225        self
226    }
227
228    /// Set the network args for the node
229    pub fn with_network(mut self, network: NetworkArgs) -> Self {
230        self.network = network;
231        self
232    }
233
234    /// Set the rpc args for the node
235    pub fn with_rpc(mut self, rpc: RpcServerArgs) -> Self {
236        self.rpc = rpc;
237        self
238    }
239
240    /// Set the txpool args for the node
241    pub fn with_txpool(mut self, txpool: TxPoolArgs) -> Self {
242        self.txpool = txpool;
243        self
244    }
245
246    /// Set the builder args for the node
247    pub fn with_payload_builder(mut self, builder: PayloadBuilderArgs) -> Self {
248        self.builder = builder;
249        self
250    }
251
252    /// Set the debug args for the node
253    pub fn with_debug(mut self, debug: DebugArgs) -> Self {
254        self.debug = debug;
255        self
256    }
257
258    /// Set the database args for the node
259    pub const fn with_db(mut self, db: DatabaseArgs) -> Self {
260        self.db = db;
261        self
262    }
263
264    /// Set the dev args for the node
265    pub const fn with_dev(mut self, dev: DevArgs) -> Self {
266        self.dev = dev;
267        self
268    }
269
270    /// Set the pruning args for the node
271    pub fn with_pruning(mut self, pruning: PruningArgs) -> Self {
272        self.pruning = pruning;
273        self
274    }
275
276    /// Returns pruning configuration.
277    pub fn prune_config(&self) -> Option<PruneConfig>
278    where
279        ChainSpec: EthChainSpec,
280    {
281        self.pruning.prune_config(&self.chain)
282    }
283
284    /// Returns the max block that the node should run to, looking it up from the network if
285    /// necessary
286    pub async fn max_block<Provider, Client>(
287        &self,
288        network_client: Client,
289        provider: Provider,
290    ) -> eyre::Result<Option<BlockNumber>>
291    where
292        Provider: HeaderProvider,
293        Client: HeadersClient<Header: reth_primitives_traits::BlockHeader>,
294    {
295        let max_block = if let Some(block) = self.debug.max_block {
296            Some(block)
297        } else if let Some(tip) = self.debug.tip {
298            Some(self.lookup_or_fetch_tip(provider, network_client, tip).await?)
299        } else {
300            None
301        };
302
303        Ok(max_block)
304    }
305
306    /// Fetches the head block from the database.
307    ///
308    /// If the database is empty, returns the genesis block.
309    pub fn lookup_head<Factory>(&self, factory: &Factory) -> ProviderResult<Head>
310    where
311        Factory: DatabaseProviderFactory<
312            Provider: HeaderProvider + StageCheckpointReader + BlockHashReader,
313        >,
314    {
315        let provider = factory.database_provider_ro()?;
316
317        let head = provider.get_stage_checkpoint(StageId::Finish)?.unwrap_or_default().block_number;
318
319        let header = provider
320            .header_by_number(head)?
321            .expect("the header for the latest block is missing, database is corrupt");
322
323        let total_difficulty = provider
324            .header_td_by_number(head)?
325            // total difficulty is effectively deprecated, but still required in some places, e.g.
326            // p2p
327            .unwrap_or_default();
328
329        let hash = provider
330            .block_hash(head)?
331            .expect("the hash for the latest block is missing, database is corrupt");
332
333        Ok(Head {
334            number: head,
335            hash,
336            difficulty: header.difficulty(),
337            total_difficulty,
338            timestamp: header.timestamp(),
339        })
340    }
341
342    /// Attempt to look up the block number for the tip hash in the database.
343    /// If it doesn't exist, download the header and return the block number.
344    ///
345    /// NOTE: The download is attempted with infinite retries.
346    pub async fn lookup_or_fetch_tip<Provider, Client>(
347        &self,
348        provider: Provider,
349        client: Client,
350        tip: B256,
351    ) -> ProviderResult<u64>
352    where
353        Provider: HeaderProvider,
354        Client: HeadersClient<Header: reth_primitives_traits::BlockHeader>,
355    {
356        let header = provider.header_by_hash_or_number(tip.into())?;
357
358        // try to look up the header in the database
359        if let Some(header) = header {
360            info!(target: "reth::cli", ?tip, "Successfully looked up tip block in the database");
361            return Ok(header.number())
362        }
363
364        Ok(self.fetch_tip_from_network(client, tip.into()).await.number())
365    }
366
367    /// Attempt to look up the block with the given number and return the header.
368    ///
369    /// NOTE: The download is attempted with infinite retries.
370    pub async fn fetch_tip_from_network<Client>(
371        &self,
372        client: Client,
373        tip: BlockHashOrNumber,
374    ) -> SealedHeader<Client::Header>
375    where
376        Client: HeadersClient<Header: reth_primitives_traits::BlockHeader>,
377    {
378        info!(target: "reth::cli", ?tip, "Fetching tip block from the network.");
379        let mut fetch_failures = 0;
380        loop {
381            match get_single_header(&client, tip).await {
382                Ok(tip_header) => {
383                    info!(target: "reth::cli", ?tip, "Successfully fetched tip");
384                    return tip_header
385                }
386                Err(error) => {
387                    fetch_failures += 1;
388                    if fetch_failures % 20 == 0 {
389                        error!(target: "reth::cli", ?fetch_failures, %error, "Failed to fetch the tip. Retrying...");
390                    }
391                }
392            }
393        }
394    }
395
396    /// Change rpc port numbers based on the instance number, using the inner
397    /// [`RpcServerArgs::adjust_instance_ports`] method.
398    pub fn adjust_instance_ports(&mut self) {
399        self.rpc.adjust_instance_ports(self.instance);
400        self.network.adjust_instance_ports(self.instance);
401    }
402
403    /// Sets networking and RPC ports to zero, causing the OS to choose random unused ports when
404    /// sockets are bound.
405    pub fn with_unused_ports(mut self) -> Self {
406        self.rpc = self.rpc.with_unused_ports();
407        self.network = self.network.with_unused_ports();
408        self
409    }
410
411    /// Resolve the final datadir path.
412    pub fn datadir(&self) -> ChainPath<DataDirPath>
413    where
414        ChainSpec: EthChainSpec,
415    {
416        self.datadir.clone().resolve_datadir(self.chain.chain())
417    }
418
419    /// Load an application configuration from a specified path.
420    ///
421    /// A new configuration file is created with default values if none
422    /// exists.
423    pub fn load_path<T: Serialize + DeserializeOwned + Default>(
424        path: impl AsRef<Path>,
425    ) -> eyre::Result<T> {
426        let path = path.as_ref();
427        match fs::read_to_string(path) {
428            Ok(cfg_string) => {
429                toml::from_str(&cfg_string).map_err(|e| eyre!("Failed to parse TOML: {e}"))
430            }
431            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
432                if let Some(parent) = path.parent() {
433                    fs::create_dir_all(parent)
434                        .map_err(|e| eyre!("Failed to create directory: {e}"))?;
435                }
436                let cfg = T::default();
437                let s = toml::to_string_pretty(&cfg)
438                    .map_err(|e| eyre!("Failed to serialize to TOML: {e}"))?;
439                fs::write(path, s).map_err(|e| eyre!("Failed to write configuration file: {e}"))?;
440                Ok(cfg)
441            }
442            Err(e) => Err(eyre!("Failed to load configuration: {e}")),
443        }
444    }
445
446    /// Modifies the [`ChainSpec`] generic of the config using the provided closure.
447    pub fn map_chainspec<F, C>(self, f: F) -> NodeConfig<C>
448    where
449        F: FnOnce(Arc<ChainSpec>) -> C,
450    {
451        let chain = Arc::new(f(self.chain));
452        NodeConfig {
453            chain,
454            datadir: self.datadir,
455            config: self.config,
456            metrics: self.metrics,
457            instance: self.instance,
458            network: self.network,
459            rpc: self.rpc,
460            txpool: self.txpool,
461            builder: self.builder,
462            debug: self.debug,
463            db: self.db,
464            dev: self.dev,
465            pruning: self.pruning,
466            engine: self.engine,
467        }
468    }
469}
470
471impl Default for NodeConfig<ChainSpec> {
472    fn default() -> Self {
473        Self::new(MAINNET.clone())
474    }
475}
476
477impl<ChainSpec> Clone for NodeConfig<ChainSpec> {
478    fn clone(&self) -> Self {
479        Self {
480            chain: self.chain.clone(),
481            config: self.config.clone(),
482            metrics: self.metrics,
483            instance: self.instance,
484            network: self.network.clone(),
485            rpc: self.rpc.clone(),
486            txpool: self.txpool.clone(),
487            builder: self.builder.clone(),
488            debug: self.debug.clone(),
489            db: self.db,
490            dev: self.dev,
491            pruning: self.pruning.clone(),
492            datadir: self.datadir.clone(),
493            engine: self.engine.clone(),
494        }
495    }
496}