Skip to main content

reth_config/
config.rs

1//! Configuration files.
2use reth_network_peers::TrustedPeer;
3use reth_network_types::{PeersConfig, SessionsConfig};
4use reth_prune_types::{PruneModes, MINIMUM_UNWIND_SAFE_DISTANCE};
5use reth_stages_types::ExecutionStageThresholds;
6use reth_static_file_types::{StaticFileMap, StaticFileSegment};
7use std::{
8    path::{Path, PathBuf},
9    time::Duration,
10};
11use url::Url;
12
13#[cfg(feature = "serde")]
14const EXTENSION: &str = "toml";
15
16/// The default prune block interval
17pub const DEFAULT_BLOCK_INTERVAL: usize = 5;
18
19/// Configuration for the reth node.
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "serde", serde(default))]
23pub struct Config {
24    /// Nodes to bootstrap P2P discovery with, as `enode://` URLs or `enr:` records.
25    ///
26    /// Takes precedence over the chain spec bootnodes, and is overridden by `--bootnodes`.
27    pub bootnodes: Vec<TrustedPeer>,
28    /// Configuration for each stage in the pipeline.
29    pub stages: StageConfig,
30    /// Configuration for pruning.
31    #[cfg_attr(feature = "serde", serde(default))]
32    pub prune: PruneConfig,
33    /// Configuration for the discovery service.
34    pub peers: PeersConfig,
35    /// Configuration for peer sessions.
36    pub sessions: SessionsConfig,
37    /// Configuration for static files.
38    #[cfg_attr(feature = "serde", serde(default))]
39    pub static_files: StaticFilesConfig,
40}
41
42impl Config {
43    /// Sets the pruning configuration.
44    pub fn set_prune_config(&mut self, prune_config: PruneConfig) {
45        self.prune = prune_config;
46    }
47}
48
49#[cfg(feature = "serde")]
50impl Config {
51    /// Load a [`Config`] from a specified path.
52    ///
53    /// A new configuration file is created with default values if none
54    /// exists.
55    pub fn from_path(path: impl AsRef<Path>) -> eyre::Result<Self> {
56        let path = path.as_ref();
57        match std::fs::read_to_string(path) {
58            Ok(cfg_string) => {
59                toml::from_str(&cfg_string).map_err(|e| eyre::eyre!("Failed to parse TOML: {e}"))
60            }
61            Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
62                if let Some(parent) = path.parent() {
63                    std::fs::create_dir_all(parent)
64                        .map_err(|e| eyre::eyre!("Failed to create directory: {e}"))?;
65                }
66                let cfg = Self::default();
67                let s = toml::to_string_pretty(&cfg)
68                    .map_err(|e| eyre::eyre!("Failed to serialize to TOML: {e}"))?;
69                std::fs::write(path, s)
70                    .map_err(|e| eyre::eyre!("Failed to write configuration file: {e}"))?;
71                Ok(cfg)
72            }
73            Err(e) => Err(eyre::eyre!("Failed to load configuration: {e}")),
74        }
75    }
76
77    /// Returns the [`PeersConfig`] for the node.
78    ///
79    /// If a peers file is provided, the basic nodes from the file are added to the configuration.
80    pub fn peers_config_with_basic_nodes_from_file(
81        &self,
82        peers_file: Option<&Path>,
83    ) -> PeersConfig {
84        self.peers
85            .clone()
86            .with_basic_nodes_from_file(peers_file)
87            .unwrap_or_else(|_| self.peers.clone())
88    }
89
90    /// Save the configuration to toml file.
91    pub fn save(&self, path: &Path) -> Result<(), std::io::Error> {
92        if path.extension() != Some(std::ffi::OsStr::new(EXTENSION)) {
93            return Err(std::io::Error::new(
94                std::io::ErrorKind::InvalidInput,
95                format!("reth config file extension must be '{EXTENSION}'"),
96            ));
97        }
98
99        std::fs::write(
100            path,
101            toml::to_string(self)
102                .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?,
103        )
104    }
105}
106
107/// Configuration for each stage in the pipeline.
108#[derive(Debug, Clone, Default, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110#[cfg_attr(feature = "serde", serde(default))]
111pub struct StageConfig {
112    /// ERA stage configuration.
113    pub era: EraConfig,
114    /// Header stage configuration.
115    pub headers: HeadersConfig,
116    /// Body stage configuration.
117    pub bodies: BodiesConfig,
118    /// Sender Recovery stage configuration.
119    pub sender_recovery: SenderRecoveryConfig,
120    /// Execution stage configuration.
121    pub execution: ExecutionConfig,
122    /// Prune stage configuration.
123    pub prune: PruneStageConfig,
124    /// Account Hashing stage configuration.
125    pub account_hashing: HashingConfig,
126    /// Storage Hashing stage configuration.
127    pub storage_hashing: HashingConfig,
128    /// Merkle stage configuration.
129    pub merkle: MerkleConfig,
130    /// Transaction Lookup stage configuration.
131    pub transaction_lookup: TransactionLookupConfig,
132    /// Index Account History stage configuration.
133    pub index_account_history: IndexHistoryConfig,
134    /// Index Storage History stage configuration.
135    pub index_storage_history: IndexHistoryConfig,
136    /// Common ETL related configuration.
137    pub etl: EtlConfig,
138}
139
140impl StageConfig {
141    /// The highest threshold (in number of blocks) for switching between incremental and full
142    /// calculations across `MerkleStage`, `AccountHashingStage` and `StorageHashingStage`. This is
143    /// required to figure out if can prune or not changesets on subsequent pipeline runs during
144    /// `ExecutionStage`
145    pub fn execution_external_clean_threshold(&self) -> u64 {
146        self.merkle
147            .incremental_threshold
148            .max(self.account_hashing.clean_threshold)
149            .max(self.storage_hashing.clean_threshold)
150    }
151}
152
153/// ERA stage configuration.
154#[derive(Debug, Clone, Default, PartialEq, Eq)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
156#[cfg_attr(feature = "serde", serde(default))]
157pub struct EraConfig {
158    /// Path to a local directory where ERA1 files are located.
159    ///
160    /// Conflicts with `url`.
161    pub path: Option<PathBuf>,
162    /// The base URL of an ERA1 file host to download from.
163    ///
164    /// Conflicts with `path`.
165    pub url: Option<Url>,
166    /// Path to a directory where files downloaded from `url` will be stored until processed.
167    ///
168    /// Required for `url`.
169    pub folder: Option<PathBuf>,
170}
171
172impl EraConfig {
173    /// Sets `folder` for temporary downloads as a directory called "era" inside `dir`.
174    pub fn with_datadir(mut self, dir: impl AsRef<Path>) -> Self {
175        self.folder = Some(dir.as_ref().join("era"));
176        self
177    }
178}
179
180/// Header stage configuration.
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
183#[cfg_attr(feature = "serde", serde(default))]
184pub struct HeadersConfig {
185    /// The maximum number of requests to send concurrently.
186    ///
187    /// Default: 100
188    pub downloader_max_concurrent_requests: usize,
189    /// The minimum number of requests to send concurrently.
190    ///
191    /// Default: 5
192    pub downloader_min_concurrent_requests: usize,
193    /// Maximum amount of responses to buffer internally.
194    /// The response contains multiple headers.
195    pub downloader_max_buffered_responses: usize,
196    /// The maximum number of headers to request from a peer at a time.
197    pub downloader_request_limit: u64,
198    /// The maximum number of headers to download before committing progress to the database.
199    pub commit_threshold: u64,
200}
201
202impl Default for HeadersConfig {
203    fn default() -> Self {
204        Self {
205            commit_threshold: 10_000,
206            downloader_request_limit: 1_000,
207            downloader_max_concurrent_requests: 100,
208            downloader_min_concurrent_requests: 5,
209            downloader_max_buffered_responses: 100,
210        }
211    }
212}
213
214/// Body stage configuration.
215#[derive(Debug, Clone, Copy, PartialEq, Eq)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217#[cfg_attr(feature = "serde", serde(default))]
218pub struct BodiesConfig {
219    /// The batch size of non-empty blocks per one request
220    ///
221    /// Default: 200
222    pub downloader_request_limit: u64,
223    /// The maximum number of block bodies returned at once from the stream
224    ///
225    /// Default: `1_000`
226    pub downloader_stream_batch_size: usize,
227    /// The size of the internal block buffer in bytes.
228    ///
229    /// Default: 2GB
230    pub downloader_max_buffered_blocks_size_bytes: usize,
231    /// The minimum number of requests to send concurrently.
232    ///
233    /// Default: 5
234    pub downloader_min_concurrent_requests: usize,
235    /// The maximum number of requests to send concurrently.
236    /// This is equal to the max number of peers.
237    ///
238    /// Default: 100
239    pub downloader_max_concurrent_requests: usize,
240}
241
242impl Default for BodiesConfig {
243    fn default() -> Self {
244        Self {
245            downloader_request_limit: 200,
246            downloader_stream_batch_size: 1_000,
247            downloader_max_buffered_blocks_size_bytes: 2 * 1024 * 1024 * 1024, // ~2GB
248            downloader_min_concurrent_requests: 5,
249            downloader_max_concurrent_requests: 100,
250        }
251    }
252}
253
254/// Sender recovery stage configuration.
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
257#[cfg_attr(feature = "serde", serde(default))]
258pub struct SenderRecoveryConfig {
259    /// The maximum number of transactions to process before committing progress to the database.
260    pub commit_threshold: u64,
261}
262
263impl Default for SenderRecoveryConfig {
264    fn default() -> Self {
265        Self { commit_threshold: 5_000_000 }
266    }
267}
268
269/// Execution stage configuration.
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
272#[cfg_attr(feature = "serde", serde(default))]
273pub struct ExecutionConfig {
274    /// The maximum number of blocks to process before the execution stage commits.
275    pub max_blocks: Option<u64>,
276    /// The maximum number of state changes to keep in memory before the execution stage commits.
277    pub max_changes: Option<u64>,
278    /// The maximum cumulative amount of gas to process before the execution stage commits.
279    pub max_cumulative_gas: Option<u64>,
280    /// The maximum time spent on blocks processing before the execution stage commits.
281    #[cfg_attr(
282        feature = "serde",
283        serde(
284            serialize_with = "humantime_serde::serialize",
285            deserialize_with = "deserialize_duration"
286        )
287    )]
288    pub max_duration: Option<Duration>,
289}
290
291impl Default for ExecutionConfig {
292    fn default() -> Self {
293        Self {
294            max_blocks: Some(500_000),
295            max_changes: Some(5_000_000),
296            // 50k full blocks of 30M gas
297            max_cumulative_gas: Some(30_000_000 * 50_000),
298            // 10 minutes
299            max_duration: Some(Duration::from_secs(10 * 60)),
300        }
301    }
302}
303
304impl From<ExecutionConfig> for ExecutionStageThresholds {
305    fn from(config: ExecutionConfig) -> Self {
306        Self {
307            max_blocks: config.max_blocks,
308            max_changes: config.max_changes,
309            max_cumulative_gas: config.max_cumulative_gas,
310            max_duration: config.max_duration,
311        }
312    }
313}
314
315/// Prune stage configuration.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
318#[cfg_attr(feature = "serde", serde(default))]
319pub struct PruneStageConfig {
320    /// The maximum number of entries to prune before committing progress to the database.
321    pub commit_threshold: usize,
322}
323
324impl Default for PruneStageConfig {
325    fn default() -> Self {
326        Self { commit_threshold: 1_000_000 }
327    }
328}
329
330/// Hashing stage configuration.
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
333#[cfg_attr(feature = "serde", serde(default))]
334pub struct HashingConfig {
335    /// The threshold (in number of blocks) for switching between
336    /// incremental hashing and full hashing.
337    pub clean_threshold: u64,
338    /// The maximum number of entities to process before committing progress to the database.
339    pub commit_threshold: u64,
340    /// The maximum number of changeset entries to process before committing progress. The stage
341    /// commits after either `commit_threshold` blocks or `commit_entries` entries, whichever
342    /// comes first. This bounds memory usage when blocks contain many state changes.
343    pub commit_entries: u64,
344}
345
346impl Default for HashingConfig {
347    fn default() -> Self {
348        Self { clean_threshold: 500_000, commit_threshold: 100_000, commit_entries: 30_000_000 }
349    }
350}
351
352/// Merkle stage configuration.
353#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
355#[cfg_attr(feature = "serde", serde(default))]
356pub struct MerkleConfig {
357    /// The number of blocks we will run the incremental root method for when we are catching up on
358    /// the merkle stage for a large number of blocks.
359    ///
360    /// When we are catching up for a large number of blocks, we can only run the incremental root
361    /// for a limited number of blocks, otherwise the incremental root method may cause the node to
362    /// OOM. This number determines how many blocks in a row we will run the incremental root
363    /// method for.
364    pub incremental_threshold: u64,
365    /// The threshold (in number of blocks) for switching from incremental trie building of changes
366    /// to whole rebuild.
367    pub rebuild_threshold: u64,
368}
369
370impl Default for MerkleConfig {
371    fn default() -> Self {
372        Self { incremental_threshold: 7_000, rebuild_threshold: 100_000 }
373    }
374}
375
376/// Transaction Lookup stage configuration.
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
378#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
379#[cfg_attr(feature = "serde", serde(default))]
380pub struct TransactionLookupConfig {
381    /// The maximum number of transactions to process before writing to disk.
382    pub chunk_size: u64,
383}
384
385impl Default for TransactionLookupConfig {
386    fn default() -> Self {
387        Self { chunk_size: 5_000_000 }
388    }
389}
390
391/// Common ETL related configuration.
392#[derive(Debug, Clone, PartialEq, Eq)]
393#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
394#[cfg_attr(feature = "serde", serde(default))]
395pub struct EtlConfig {
396    /// Data directory where temporary files are created.
397    pub dir: Option<PathBuf>,
398    /// The maximum size in bytes of data held in memory before being flushed to disk as a file.
399    pub file_size: usize,
400}
401
402impl Default for EtlConfig {
403    fn default() -> Self {
404        Self { dir: None, file_size: Self::default_file_size() }
405    }
406}
407
408impl EtlConfig {
409    /// Creates an ETL configuration
410    pub const fn new(dir: Option<PathBuf>, file_size: usize) -> Self {
411        Self { dir, file_size }
412    }
413
414    /// Return default ETL directory from datadir path.
415    pub fn from_datadir(path: &Path) -> PathBuf {
416        path.join("etl-tmp")
417    }
418
419    /// Default size in bytes of data held in memory before being flushed to disk as a file.
420    pub const fn default_file_size() -> usize {
421        // 500 MB
422        500 * (1024 * 1024)
423    }
424}
425
426/// Static files configuration.
427#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
428#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
429#[cfg_attr(feature = "serde", serde(default))]
430pub struct StaticFilesConfig {
431    /// Number of blocks per file for each segment.
432    pub blocks_per_file: BlocksPerFileConfig,
433}
434
435/// Configuration for the number of blocks per file for each segment.
436#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
437#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
438#[cfg_attr(feature = "serde", serde(default))]
439pub struct BlocksPerFileConfig {
440    /// Number of blocks per file for the headers segment.
441    pub headers: Option<u64>,
442    /// Number of blocks per file for the transactions segment.
443    pub transactions: Option<u64>,
444    /// Number of blocks per file for the receipts segment.
445    pub receipts: Option<u64>,
446    /// Number of blocks per file for the transaction senders segment.
447    pub transaction_senders: Option<u64>,
448    /// Number of blocks per file for the account changesets segment.
449    pub account_change_sets: Option<u64>,
450    /// Number of blocks per file for the storage changesets segment.
451    pub storage_change_sets: Option<u64>,
452}
453
454impl StaticFilesConfig {
455    /// Validates the static files configuration.
456    ///
457    /// Returns an error if any blocks per file value is zero.
458    pub fn validate(&self) -> eyre::Result<()> {
459        let BlocksPerFileConfig {
460            headers,
461            transactions,
462            receipts,
463            transaction_senders,
464            account_change_sets,
465            storage_change_sets,
466        } = self.blocks_per_file;
467        eyre::ensure!(headers != Some(0), "Headers segment blocks per file must be greater than 0");
468        eyre::ensure!(
469            transactions != Some(0),
470            "Transactions segment blocks per file must be greater than 0"
471        );
472        eyre::ensure!(
473            receipts != Some(0),
474            "Receipts segment blocks per file must be greater than 0"
475        );
476        eyre::ensure!(
477            transaction_senders != Some(0),
478            "Transaction senders segment blocks per file must be greater than 0"
479        );
480        eyre::ensure!(
481            account_change_sets != Some(0),
482            "Account changesets segment blocks per file must be greater than 0"
483        );
484        eyre::ensure!(
485            storage_change_sets != Some(0),
486            "Storage changesets segment blocks per file must be greater than 0"
487        );
488        Ok(())
489    }
490
491    /// Converts the blocks per file configuration into a [`StaticFileMap`].
492    pub fn as_blocks_per_file_map(&self) -> StaticFileMap<u64> {
493        let BlocksPerFileConfig {
494            headers,
495            transactions,
496            receipts,
497            transaction_senders,
498            account_change_sets,
499            storage_change_sets,
500        } = self.blocks_per_file;
501
502        let mut map = StaticFileMap::default();
503        // Iterating over all possible segments allows us to do an exhaustive match here,
504        // to not forget to configure new segments in the future.
505        for segment in StaticFileSegment::iter() {
506            let blocks_per_file = match segment {
507                StaticFileSegment::Headers => headers,
508                StaticFileSegment::Transactions => transactions,
509                StaticFileSegment::Receipts => receipts,
510                StaticFileSegment::TransactionSenders => transaction_senders,
511                StaticFileSegment::AccountChangeSets => account_change_sets,
512                StaticFileSegment::StorageChangeSets => storage_change_sets,
513            };
514
515            if let Some(blocks_per_file) = blocks_per_file {
516                map.insert(segment, blocks_per_file);
517            }
518        }
519        map
520    }
521}
522
523/// History stage configuration.
524#[derive(Debug, Clone, Copy, PartialEq, Eq)]
525#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
526#[cfg_attr(feature = "serde", serde(default))]
527pub struct IndexHistoryConfig {
528    /// The maximum number of blocks to process before committing progress to the database.
529    pub commit_threshold: u64,
530}
531
532impl Default for IndexHistoryConfig {
533    fn default() -> Self {
534        Self { commit_threshold: 100_000 }
535    }
536}
537
538/// Pruning configuration.
539#[derive(Debug, Clone, PartialEq, Eq)]
540#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
541#[cfg_attr(feature = "serde", serde(default))]
542pub struct PruneConfig {
543    /// Minimum pruning interval measured in blocks.
544    pub block_interval: usize,
545    /// Pruning configuration for every part of the data that can be pruned.
546    #[cfg_attr(feature = "serde", serde(alias = "parts"))]
547    pub segments: PruneModes,
548    /// Minimum distance from the tip required for pruning. Controls the safety margin for
549    /// reorgs and manual unwinds. Defaults to [`MINIMUM_UNWIND_SAFE_DISTANCE`].
550    #[cfg_attr(feature = "serde", serde(default = "default_minimum_pruning_distance"))]
551    pub minimum_pruning_distance: u64,
552}
553
554/// Returns the default minimum pruning distance.
555const fn default_minimum_pruning_distance() -> u64 {
556    MINIMUM_UNWIND_SAFE_DISTANCE
557}
558
559impl Default for PruneConfig {
560    fn default() -> Self {
561        Self {
562            block_interval: DEFAULT_BLOCK_INTERVAL,
563            segments: PruneModes::default(),
564            minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE,
565        }
566    }
567}
568
569impl PruneConfig {
570    /// Returns whether this configuration is the default one.
571    pub fn is_default(&self) -> bool {
572        self == &Self::default()
573    }
574
575    /// Returns whether there is any kind of receipt pruning configuration.
576    pub fn has_receipts_pruning(&self) -> bool {
577        self.segments.has_receipts_pruning()
578    }
579
580    /// Merges values from `other` into `self`.
581    /// - `Option<PruneMode>` fields: set from `other` only if `self` is `None`.
582    /// - `block_interval`: set from `other` only if `self.block_interval ==
583    ///   DEFAULT_BLOCK_INTERVAL`.
584    /// - `receipts_log_filter`: set from `other` only if `self` is empty and `other` is non-empty.
585    pub fn merge(&mut self, other: Self) {
586        let Self {
587            block_interval,
588            segments:
589                PruneModes {
590                    sender_recovery,
591                    transaction_lookup,
592                    receipts,
593                    account_history,
594                    storage_history,
595                    bodies_history,
596                    receipts_log_filter,
597                },
598            minimum_pruning_distance,
599        } = other;
600
601        // Merge block_interval, only update if it's the default interval
602        if self.block_interval == DEFAULT_BLOCK_INTERVAL {
603            self.block_interval = block_interval;
604        }
605
606        // Merge minimum_pruning_distance, only update if it's the default
607        if self.minimum_pruning_distance == MINIMUM_UNWIND_SAFE_DISTANCE {
608            self.minimum_pruning_distance = minimum_pruning_distance;
609        }
610
611        // Merge the various segment prune modes
612        self.segments.sender_recovery = self.segments.sender_recovery.or(sender_recovery);
613        self.segments.transaction_lookup = self.segments.transaction_lookup.or(transaction_lookup);
614        self.segments.receipts = self.segments.receipts.or(receipts);
615        self.segments.account_history = self.segments.account_history.or(account_history);
616        self.segments.storage_history = self.segments.storage_history.or(storage_history);
617        self.segments.bodies_history = self.segments.bodies_history.or(bodies_history);
618
619        if self.segments.receipts_log_filter.0.is_empty() && !receipts_log_filter.0.is_empty() {
620            self.segments.receipts_log_filter = receipts_log_filter;
621        }
622    }
623}
624
625/// Helper type to support older versions of Duration deserialization.
626#[cfg(feature = "serde")]
627fn deserialize_duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
628where
629    D: serde::de::Deserializer<'de>,
630{
631    #[derive(serde::Deserialize)]
632    #[serde(untagged)]
633    enum AnyDuration {
634        #[serde(deserialize_with = "humantime_serde::deserialize")]
635        Human(Option<Duration>),
636        Duration(Option<Duration>),
637    }
638
639    <AnyDuration as serde::Deserialize>::deserialize(deserializer).map(|d| match d {
640        AnyDuration::Human(duration) | AnyDuration::Duration(duration) => duration,
641    })
642}
643
644#[cfg(all(test, feature = "serde"))]
645mod tests {
646    use super::{Config, EXTENSION};
647    use crate::PruneConfig;
648    use alloy_primitives::Address;
649    use reth_network_peers::TrustedPeer;
650    use reth_prune_types::{
651        PruneMode, PruneModes, ReceiptsLogPruneConfig, MINIMUM_UNWIND_SAFE_DISTANCE,
652    };
653    use std::{collections::BTreeMap, path::Path, str::FromStr, time::Duration};
654
655    fn with_tempdir(filename: &str, proc: fn(&std::path::Path)) {
656        let temp_dir = tempfile::tempdir().unwrap();
657        let config_path = temp_dir.path().join(filename).with_extension(EXTENSION);
658
659        proc(&config_path);
660
661        temp_dir.close().unwrap()
662    }
663
664    /// Run a test function with a temporary config path as fixture.
665    fn with_config_path(test_fn: fn(&Path)) {
666        // Create a temporary directory for the config file
667        let config_dir = tempfile::tempdir().expect("creating test fixture failed");
668        // Create the config file path
669        let config_path =
670            config_dir.path().join("example-app").join("example-config").with_extension("toml");
671        // Run the test function with the config path
672        test_fn(&config_path);
673        config_dir.close().expect("removing test fixture failed");
674    }
675
676    #[test]
677    fn test_load_path_works() {
678        with_config_path(|path| {
679            let config = Config::from_path(path).expect("load_path failed");
680            assert_eq!(config, Config::default());
681        })
682    }
683
684    #[test]
685    fn test_load_path_reads_existing_config() {
686        with_config_path(|path| {
687            let config = Config::default();
688
689            // Create the parent directory if it doesn't exist
690            if let Some(parent) = path.parent() {
691                std::fs::create_dir_all(parent).expect("Failed to create directories");
692            }
693
694            // Write the config to the file
695            std::fs::write(path, toml::to_string(&config).unwrap())
696                .expect("Failed to write config");
697
698            // Load the config from the file and compare it
699            let loaded = Config::from_path(path).expect("load_path failed");
700            assert_eq!(config, loaded);
701        })
702    }
703
704    #[test]
705    fn test_load_path_fails_on_invalid_toml() {
706        with_config_path(|path| {
707            let invalid_toml = "invalid toml data";
708
709            // Create the parent directory if it doesn't exist
710            if let Some(parent) = path.parent() {
711                std::fs::create_dir_all(parent).expect("Failed to create directories");
712            }
713
714            // Write invalid TOML data to the file
715            std::fs::write(path, invalid_toml).expect("Failed to write invalid TOML");
716
717            // Attempt to load the config should fail
718            let result = Config::from_path(path);
719            assert!(result.is_err());
720        })
721    }
722
723    #[test]
724    fn test_load_path_creates_directory_if_not_exists() {
725        with_config_path(|path| {
726            // Ensure the directory does not exist
727            let parent = path.parent().unwrap();
728            assert!(!parent.exists());
729
730            // Load the configuration, which should create the directory and a default config file
731            let config = Config::from_path(path).expect("load_path failed");
732            assert_eq!(config, Config::default());
733
734            // The directory and file should now exist
735            assert!(parent.exists());
736            assert!(path.exists());
737        });
738    }
739
740    #[test]
741    fn test_store_config() {
742        with_tempdir("config-store-test", |config_path| {
743            let config = Config::default();
744            std::fs::write(
745                config_path,
746                toml::to_string(&config).expect("Failed to serialize config"),
747            )
748            .expect("Failed to write config file");
749        })
750    }
751
752    #[test]
753    fn test_store_config_method() {
754        with_tempdir("config-store-test-method", |config_path| {
755            let config = Config::default();
756            config.save(config_path).expect("Failed to store config");
757        })
758    }
759
760    #[test]
761    fn test_load_config() {
762        with_tempdir("config-load-test", |config_path| {
763            let config = Config::default();
764
765            // Write the config to a file
766            std::fs::write(
767                config_path,
768                toml::to_string(&config).expect("Failed to serialize config"),
769            )
770            .expect("Failed to write config file");
771
772            // Load the config from the file
773            let loaded_config = Config::from_path(config_path).unwrap();
774
775            // Compare the loaded config with the original config
776            assert_eq!(config, loaded_config);
777        })
778    }
779
780    #[test]
781    fn test_load_execution_stage() {
782        with_tempdir("config-load-test", |config_path| {
783            let mut config = Config::default();
784            config.stages.execution.max_duration = Some(Duration::from_secs(10 * 60));
785
786            // Write the config to a file
787            std::fs::write(
788                config_path,
789                toml::to_string(&config).expect("Failed to serialize config"),
790            )
791            .expect("Failed to write config file");
792
793            // Load the config from the file
794            let loaded_config = Config::from_path(config_path).unwrap();
795
796            // Compare the loaded config with the original config
797            assert_eq!(config, loaded_config);
798        })
799    }
800
801    // ensures config deserialization is backwards compatible
802    #[test]
803    fn test_backwards_compatibility() {
804        let alpha_0_0_8 = r"#
805[stages.headers]
806downloader_max_concurrent_requests = 100
807downloader_min_concurrent_requests = 5
808downloader_max_buffered_responses = 100
809downloader_request_limit = 1000
810commit_threshold = 10000
811
812[stages.bodies]
813downloader_request_limit = 200
814downloader_stream_batch_size = 1000
815downloader_max_buffered_blocks_size_bytes = 2147483648
816downloader_min_concurrent_requests = 5
817downloader_max_concurrent_requests = 100
818
819[stages.sender_recovery]
820commit_threshold = 5000000
821
822[stages.execution]
823max_blocks = 500000
824max_changes = 5000000
825
826[stages.account_hashing]
827clean_threshold = 500000
828commit_threshold = 100000
829
830[stages.storage_hashing]
831clean_threshold = 500000
832commit_threshold = 100000
833
834[stages.merkle]
835clean_threshold = 50000
836
837[stages.transaction_lookup]
838chunk_size = 5000000
839
840[stages.index_account_history]
841commit_threshold = 100000
842
843[stages.index_storage_history]
844commit_threshold = 100000
845
846[peers]
847refill_slots_interval = '1s'
848trusted_nodes = []
849connect_trusted_nodes_only = false
850max_backoff_count = 5
851ban_duration = '12h'
852
853[peers.connection_info]
854max_outbound = 100
855max_inbound = 30
856
857[peers.reputation_weights]
858bad_message = -16384
859bad_block = -16384
860bad_transactions = -16384
861already_seen_transactions = 0
862timeout = -4096
863bad_protocol = -2147483648
864failed_to_connect = -25600
865dropped = -4096
866
867[peers.backoff_durations]
868low = '30s'
869medium = '3m'
870high = '15m'
871max = '1h'
872
873[sessions]
874session_command_buffer = 32
875session_event_buffer = 260
876
877[sessions.limits]
878
879[sessions.initial_internal_request_timeout]
880secs = 20
881nanos = 0
882
883[sessions.protocol_breach_request_timeout]
884secs = 120
885nanos = 0
886
887[prune]
888block_interval = 5
889
890[prune.parts]
891sender_recovery = { distance = 16384 }
892transaction_lookup = 'full'
893receipts = { before = 1920000 }
894account_history = { distance = 16384 }
895storage_history = { distance = 16384 }
896[prune.parts.receipts_log_filter]
897'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' = { before = 17000000 }
898'0xdac17f958d2ee523a2206206994597c13d831ec7' = { distance = 1000 }
899#";
900        let _conf: Config = toml::from_str(alpha_0_0_8).unwrap();
901
902        let alpha_0_0_11 = r"#
903[prune.segments]
904sender_recovery = { distance = 16384 }
905transaction_lookup = 'full'
906receipts = { before = 1920000 }
907account_history = { distance = 16384 }
908storage_history = { distance = 16384 }
909[prune.segments.receipts_log_filter]
910'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' = { before = 17000000 }
911'0xdac17f958d2ee523a2206206994597c13d831ec7' = { distance = 1000 }
912#";
913        let _conf: Config = toml::from_str(alpha_0_0_11).unwrap();
914
915        let alpha_0_0_18 = r"#
916[stages.headers]
917downloader_max_concurrent_requests = 100
918downloader_min_concurrent_requests = 5
919downloader_max_buffered_responses = 100
920downloader_request_limit = 1000
921commit_threshold = 10000
922
923[stages.total_difficulty]
924commit_threshold = 100000
925
926[stages.bodies]
927downloader_request_limit = 200
928downloader_stream_batch_size = 1000
929downloader_max_buffered_blocks_size_bytes = 2147483648
930downloader_min_concurrent_requests = 5
931downloader_max_concurrent_requests = 100
932
933[stages.sender_recovery]
934commit_threshold = 5000000
935
936[stages.execution]
937max_blocks = 500000
938max_changes = 5000000
939max_cumulative_gas = 1500000000000
940[stages.execution.max_duration]
941secs = 600
942nanos = 0
943
944[stages.account_hashing]
945clean_threshold = 500000
946commit_threshold = 100000
947
948[stages.storage_hashing]
949clean_threshold = 500000
950commit_threshold = 100000
951
952[stages.merkle]
953clean_threshold = 50000
954
955[stages.transaction_lookup]
956commit_threshold = 5000000
957
958[stages.index_account_history]
959commit_threshold = 100000
960
961[stages.index_storage_history]
962commit_threshold = 100000
963
964[peers]
965refill_slots_interval = '5s'
966trusted_nodes = []
967connect_trusted_nodes_only = false
968max_backoff_count = 5
969ban_duration = '12h'
970
971[peers.connection_info]
972max_outbound = 100
973max_inbound = 30
974max_concurrent_outbound_dials = 10
975
976[peers.reputation_weights]
977bad_message = -16384
978bad_block = -16384
979bad_transactions = -16384
980already_seen_transactions = 0
981timeout = -4096
982bad_protocol = -2147483648
983failed_to_connect = -25600
984dropped = -4096
985bad_announcement = -1024
986
987[peers.backoff_durations]
988low = '30s'
989medium = '3m'
990high = '15m'
991max = '1h'
992
993[sessions]
994session_command_buffer = 32
995session_event_buffer = 260
996
997[sessions.limits]
998
999[sessions.initial_internal_request_timeout]
1000secs = 20
1001nanos = 0
1002
1003[sessions.protocol_breach_request_timeout]
1004secs = 120
1005nanos = 0
1006#";
1007        let conf: Config = toml::from_str(alpha_0_0_18).unwrap();
1008        assert_eq!(conf.stages.execution.max_duration, Some(Duration::from_secs(10 * 60)));
1009
1010        let alpha_0_0_19 = r"#
1011[stages.headers]
1012downloader_max_concurrent_requests = 100
1013downloader_min_concurrent_requests = 5
1014downloader_max_buffered_responses = 100
1015downloader_request_limit = 1000
1016commit_threshold = 10000
1017
1018[stages.total_difficulty]
1019commit_threshold = 100000
1020
1021[stages.bodies]
1022downloader_request_limit = 200
1023downloader_stream_batch_size = 1000
1024downloader_max_buffered_blocks_size_bytes = 2147483648
1025downloader_min_concurrent_requests = 5
1026downloader_max_concurrent_requests = 100
1027
1028[stages.sender_recovery]
1029commit_threshold = 5000000
1030
1031[stages.execution]
1032max_blocks = 500000
1033max_changes = 5000000
1034max_cumulative_gas = 1500000000000
1035max_duration = '10m'
1036
1037[stages.account_hashing]
1038clean_threshold = 500000
1039commit_threshold = 100000
1040
1041[stages.storage_hashing]
1042clean_threshold = 500000
1043commit_threshold = 100000
1044
1045[stages.merkle]
1046clean_threshold = 50000
1047
1048[stages.transaction_lookup]
1049commit_threshold = 5000000
1050
1051[stages.index_account_history]
1052commit_threshold = 100000
1053
1054[stages.index_storage_history]
1055commit_threshold = 100000
1056
1057[peers]
1058refill_slots_interval = '5s'
1059trusted_nodes = []
1060connect_trusted_nodes_only = false
1061max_backoff_count = 5
1062ban_duration = '12h'
1063
1064[peers.connection_info]
1065max_outbound = 100
1066max_inbound = 30
1067max_concurrent_outbound_dials = 10
1068
1069[peers.reputation_weights]
1070bad_message = -16384
1071bad_block = -16384
1072bad_transactions = -16384
1073already_seen_transactions = 0
1074timeout = -4096
1075bad_protocol = -2147483648
1076failed_to_connect = -25600
1077dropped = -4096
1078bad_announcement = -1024
1079
1080[peers.backoff_durations]
1081low = '30s'
1082medium = '3m'
1083high = '15m'
1084max = '1h'
1085
1086[sessions]
1087session_command_buffer = 32
1088session_event_buffer = 260
1089
1090[sessions.limits]
1091
1092[sessions.initial_internal_request_timeout]
1093secs = 20
1094nanos = 0
1095
1096[sessions.protocol_breach_request_timeout]
1097secs = 120
1098nanos = 0
1099#";
1100        let _conf: Config = toml::from_str(alpha_0_0_19).unwrap();
1101    }
1102
1103    // ensures prune config deserialization is backwards compatible
1104    #[test]
1105    fn test_backwards_compatibility_prune_full() {
1106        let s = r"#
1107[prune]
1108block_interval = 5
1109
1110[prune.segments]
1111sender_recovery = { distance = 16384 }
1112transaction_lookup = 'full'
1113receipts = { distance = 16384 }
1114#";
1115        let _conf: Config = toml::from_str(s).unwrap();
1116    }
1117
1118    #[test]
1119    fn test_prune_config_merge() {
1120        let mut config1 = PruneConfig {
1121            block_interval: 5,
1122            minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE,
1123            segments: PruneModes {
1124                sender_recovery: Some(PruneMode::Full),
1125                transaction_lookup: None,
1126                receipts: Some(PruneMode::Distance(1000)),
1127                account_history: None,
1128                storage_history: Some(PruneMode::Before(5000)),
1129                bodies_history: None,
1130                receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([(
1131                    Address::random(),
1132                    PruneMode::Full,
1133                )])),
1134            },
1135        };
1136
1137        let config2 = PruneConfig {
1138            block_interval: 10,
1139            minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE,
1140            segments: PruneModes {
1141                sender_recovery: Some(PruneMode::Distance(500)),
1142                transaction_lookup: Some(PruneMode::Full),
1143                receipts: Some(PruneMode::Full),
1144                account_history: Some(PruneMode::Distance(2000)),
1145                storage_history: Some(PruneMode::Distance(3000)),
1146                bodies_history: None,
1147                receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([
1148                    (Address::random(), PruneMode::Distance(1000)),
1149                    (Address::random(), PruneMode::Before(2000)),
1150                ])),
1151            },
1152        };
1153
1154        let original_filter = config1.segments.receipts_log_filter.clone();
1155        config1.merge(config2);
1156
1157        // Check that the configuration has been merged. Any configuration present in config1
1158        // should not be overwritten by config2
1159        assert_eq!(config1.block_interval, 10);
1160        assert_eq!(config1.segments.sender_recovery, Some(PruneMode::Full));
1161        assert_eq!(config1.segments.transaction_lookup, Some(PruneMode::Full));
1162        assert_eq!(config1.segments.receipts, Some(PruneMode::Distance(1000)));
1163        assert_eq!(config1.segments.account_history, Some(PruneMode::Distance(2000)));
1164        assert_eq!(config1.segments.storage_history, Some(PruneMode::Before(5000)));
1165        assert_eq!(config1.segments.receipts_log_filter, original_filter);
1166    }
1167
1168    #[test]
1169    fn test_conf_trust_nodes_only() {
1170        let trusted_nodes_only = r"#
1171[peers]
1172trusted_nodes_only = true
1173#";
1174        let conf: Config = toml::from_str(trusted_nodes_only).unwrap();
1175        assert!(conf.peers.trusted_nodes_only);
1176
1177        let trusted_nodes_only = r"#
1178[peers]
1179connect_trusted_nodes_only = true
1180#";
1181        let conf: Config = toml::from_str(trusted_nodes_only).unwrap();
1182        assert!(conf.peers.trusted_nodes_only);
1183    }
1184
1185    #[test]
1186    fn test_can_support_dns_in_trusted_nodes() {
1187        let reth_toml = r#"
1188    [peers]
1189    trusted_nodes = [
1190        "enode://0401e494dbd0c84c5c0f72adac5985d2f2525e08b68d448958aae218f5ac8198a80d1498e0ebec2ce38b1b18d6750f6e61a56b4614c5a6c6cf0981c39aed47dc@34.159.32.127:30303",
1191        "enode://e9675164b5e17b9d9edf0cc2bd79e6b6f487200c74d1331c220abb5b8ee80c2eefbf18213989585e9d0960683e819542e11d4eefb5f2b4019e1e49f9fd8fff18@berav2-bootnode.staketab.org:30303"
1192    ]
1193    "#;
1194
1195        let conf: Config = toml::from_str(reth_toml).unwrap();
1196        assert_eq!(conf.peers.trusted_nodes.len(), 2);
1197
1198        let expected_enodes = vec![
1199            "enode://0401e494dbd0c84c5c0f72adac5985d2f2525e08b68d448958aae218f5ac8198a80d1498e0ebec2ce38b1b18d6750f6e61a56b4614c5a6c6cf0981c39aed47dc@34.159.32.127:30303",
1200            "enode://e9675164b5e17b9d9edf0cc2bd79e6b6f487200c74d1331c220abb5b8ee80c2eefbf18213989585e9d0960683e819542e11d4eefb5f2b4019e1e49f9fd8fff18@berav2-bootnode.staketab.org:30303",
1201        ];
1202
1203        for enode in expected_enodes {
1204            let node = TrustedPeer::from_str(enode).unwrap();
1205            assert!(conf.peers.trusted_nodes.contains(&node));
1206        }
1207    }
1208
1209    #[test]
1210    fn test_bootnodes() {
1211        let reth_toml = r#"
1212    bootnodes = [
1213        "enode://0401e494dbd0c84c5c0f72adac5985d2f2525e08b68d448958aae218f5ac8198a80d1498e0ebec2ce38b1b18d6750f6e61a56b4614c5a6c6cf0981c39aed47dc@34.159.32.127:30303",
1214        "enode://e9675164b5e17b9d9edf0cc2bd79e6b6f487200c74d1331c220abb5b8ee80c2eefbf18213989585e9d0960683e819542e11d4eefb5f2b4019e1e49f9fd8fff18@berav2-bootnode.staketab.org:30303",
1215        "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"
1216    ]
1217    "#;
1218
1219        let conf: Config = toml::from_str(reth_toml).unwrap();
1220        assert_eq!(conf.bootnodes.len(), 3);
1221        assert_eq!(
1222            conf.bootnodes[1].host,
1223            url::Host::<String>::Domain("berav2-bootnode.staketab.org".to_string())
1224        );
1225        // the ENR omits the tcp key, so the udp port is used for the RLPx dial guess
1226        assert_eq!(
1227            conf.bootnodes[2],
1228            TrustedPeer::from_str("enode://ca634cae0d49acb401d8a4c6b6fe8c55b70d115bf400769cc1400f3258cd31387574077f301b421bc84df7266c44e9e6d569fc56be00812904767bf5ccd1fc7f@127.0.0.1:30303").unwrap()
1229        );
1230
1231        // bootnodes are written back as enode URLs, including the ones read as ENRs
1232        let serialized = toml::to_string(&conf).unwrap();
1233        assert_eq!(toml::from_str::<Config>(&serialized).unwrap(), conf);
1234    }
1235
1236    #[test]
1237    fn test_bootnodes_default_empty() {
1238        let conf: Config = toml::from_str("").unwrap();
1239        assert!(conf.bootnodes.is_empty());
1240    }
1241}