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