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