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