Skip to main content

reth_node_builder/launch/
common.rs

1//! Helper types that can be used by launchers.
2//!
3//! ## Launch Context Type System
4//!
5//! The node launch process uses a type-state pattern to ensure correct initialization
6//! order at compile time. Methods are only available when their prerequisites are met.
7//!
8//! ### Core Types
9//!
10//! - [`LaunchContext`]: Base context with executor and data directory
11//! - [`LaunchContextWith<T>`]: Context with an attached value of type `T`
12//! - [`Attached<L, R>`]: Pairs values, preserving both previous (L) and new (R) state
13//!
14//! ### Helper Attachments
15//!
16//! - [`WithConfigs`]: Node config + TOML config
17//! - [`WithMeteredProvider`]: Provider factory with metrics
18//! - [`WithMeteredProviders`]: Provider factory + blockchain provider
19//! - [`WithComponents`]: Final form with all components
20//!
21//! ### Method Availability
22//!
23//! Methods are implemented on specific type combinations:
24//! - `impl<T> LaunchContextWith<T>`: Generic methods available for any attachment
25//! - `impl LaunchContextWith<WithConfigs>`: Config-specific methods
26//! - `impl LaunchContextWith<Attached<WithConfigs, DB>>`: Database operations
27//! - `impl LaunchContextWith<Attached<WithConfigs, ProviderFactory>>`: Provider operations
28//! - etc.
29//!
30//! This ensures correct initialization order without runtime checks.
31
32use crate::{
33    components::{NodeComponents, NodeComponentsBuilder},
34    hooks::OnComponentInitializedHook,
35    BuilderContext, ExExLauncher, NodeAdapter, PrimitivesTy,
36};
37use alloy_eips::eip2124::Head;
38use alloy_primitives::{BlockNumber, B256};
39use eyre::Context;
40use rayon::ThreadPoolBuilder;
41use reth_chainspec::{Chain, EthChainSpec, EthereumHardforks};
42use reth_config::{config::EtlConfig, PruneConfig};
43use reth_consensus::noop::NoopConsensus;
44use reth_db_api::{
45    database::Database, database_metrics::DatabaseMetrics, models::PartialStateTrieUnwindMarker,
46};
47use reth_db_common::init::{
48    init_genesis_with_settings, init_genesis_with_settings_and_validate, InitStorageError,
49};
50use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader};
51use reth_engine_local::MiningMode;
52use reth_evm::{noop::NoopEvmConfig, ConfigureEvm};
53use reth_exex::ExExManagerHandle;
54use reth_fs_util as fs;
55use reth_network_p2p::headers::client::HeadersClient;
56use reth_node_api::{FullNodeTypes, NodeTypes, NodeTypesWithDB, NodeTypesWithDBAdapter};
57use reth_node_core::{
58    args::{DefaultEraHost, PruneConfigKind},
59    dirs::{ChainPath, DataDirPath},
60    node_config::NodeConfig,
61    primitives::BlockHeader,
62    version::version_metadata,
63};
64use reth_node_metrics::{
65    chain::ChainSpecInfo,
66    hooks::Hooks,
67    recorder::install_prometheus_recorder,
68    server::{MetricServer, MetricServerConfig},
69    storage::StorageSettingsInfo,
70    version::VersionInfo,
71};
72use reth_provider::{
73    providers::{NodeTypesForProvider, ProviderNodeTypes, RocksDBProvider, StaticFileProvider},
74    BalStoreHandle, BlockHashReader, BlockNumReader, DBProvider, DatabaseProviderFactory,
75    MetadataProvider, MetadataWriter, ProviderError, ProviderFactory, ProviderResult,
76    RocksDBBalStore, RocksDBProviderFactory, StageCheckpointReader, StaticFileProviderBuilder,
77    StaticFileProviderFactory, StorageSettingsCache,
78};
79use reth_prune::{PruneMode, PruneModes, PrunerBuilder};
80use reth_rpc_builder::config::RethRpcServerConfig;
81use reth_rpc_layer::JwtSecret;
82use reth_stages::{
83    sets::DefaultStages,
84    stages::{EraImportSource, MerkleStage},
85    MetricEvent, PipelineBuilder, PipelineTarget, StageId, StageSet,
86};
87use reth_static_file::{blocks_per_file_for_prune_distance, StaticFileProducer, StaticFileSegment};
88use reth_storage_overlay::OverlayManager;
89use reth_tasks::TaskExecutor;
90use reth_tracing::{
91    throttle,
92    tracing::{debug, error, info, warn},
93};
94use reth_transaction_pool::TransactionPool;
95use std::{num::NonZeroUsize, sync::Arc, thread::available_parallelism, time::Duration};
96use tokio::sync::{
97    mpsc::{unbounded_channel, UnboundedSender},
98    oneshot, watch,
99};
100
101use futures::{future::Either, stream, Stream, StreamExt};
102use reth_node_ethstats::EthStatsService;
103use reth_node_events::{cl::ConsensusLayerHealthEvents, node::NodeEvent};
104
105/// Reusable setup for launching a node.
106///
107/// This is the entry point for the node launch process. It implements a builder
108/// pattern using type-state programming to enforce correct initialization order.
109///
110/// ## Type Evolution
111///
112/// Starting from `LaunchContext`, each method transforms the type to reflect
113/// accumulated state:
114///
115/// ```text
116/// LaunchContext
117///   └─> LaunchContextWith<WithConfigs>
118///       └─> LaunchContextWith<Attached<WithConfigs, DB>>
119///           └─> LaunchContextWith<Attached<WithConfigs, ProviderFactory>>
120///               └─> LaunchContextWith<Attached<WithConfigs, WithMeteredProviders>>
121///                   └─> LaunchContextWith<Attached<WithConfigs, WithComponents>>
122/// ```
123#[derive(Debug, Clone)]
124pub struct LaunchContext {
125    /// The task executor for the node.
126    pub task_executor: TaskExecutor,
127    /// The data directory for the node.
128    pub data_dir: ChainPath<DataDirPath>,
129}
130
131impl LaunchContext {
132    /// Create a new instance of the default node launcher.
133    pub const fn new(task_executor: TaskExecutor, data_dir: ChainPath<DataDirPath>) -> Self {
134        Self { task_executor, data_dir }
135    }
136
137    /// Create launch context with attachment.
138    pub const fn with<T>(self, attachment: T) -> LaunchContextWith<T> {
139        LaunchContextWith { inner: self, attachment }
140    }
141
142    /// Loads the reth config with the configured `data_dir` and overrides settings according to the
143    /// `config`.
144    ///
145    /// Attaches both the `NodeConfig` and the loaded `reth.toml` config to the launch context.
146    pub fn with_loaded_toml_config<ChainSpec>(
147        self,
148        config: NodeConfig<ChainSpec>,
149    ) -> eyre::Result<LaunchContextWith<WithConfigs<ChainSpec>>>
150    where
151        ChainSpec: EthChainSpec + reth_chainspec::EthereumHardforks,
152    {
153        let toml_config = self.load_toml_config(&config)?;
154        Ok(self.with(WithConfigs { config, toml_config }))
155    }
156
157    /// Loads the reth config with the configured `data_dir` and overrides settings according to the
158    /// `config`.
159    ///
160    /// This is async because the trusted peers may have to be resolved.
161    pub fn load_toml_config<ChainSpec>(
162        &self,
163        config: &NodeConfig<ChainSpec>,
164    ) -> eyre::Result<reth_config::Config>
165    where
166        ChainSpec: EthChainSpec + reth_chainspec::EthereumHardforks,
167    {
168        let config_path = config.config.clone().unwrap_or_else(|| self.data_dir.config());
169
170        let mut toml_config = reth_config::Config::from_path(&config_path)
171            .wrap_err_with(|| format!("Could not load config file {config_path:?}"))?;
172
173        Self::save_pruning_config(&mut toml_config, config, &config_path)?;
174
175        info!(target: "reth::cli", path = ?config_path, "Configuration loaded");
176
177        // Update the config with the command line arguments. Only override when the CLI flag is
178        // set, so the TOML value is preserved when the flag is not passed.
179        toml_config.peers.trusted_nodes_only |= config.network.trusted_only;
180
181        // Merge static file CLI arguments with config file, giving priority to CLI
182        toml_config.static_files =
183            config.static_files.merge_with_config(toml_config.static_files, config.pruning.minimal);
184
185        Ok(toml_config)
186    }
187
188    /// Save prune config to the toml file if node is a full node or has custom pruning CLI
189    /// arguments. Also migrates deprecated prune config values to new defaults.
190    fn save_pruning_config<ChainSpec>(
191        reth_config: &mut reth_config::Config,
192        config: &NodeConfig<ChainSpec>,
193        config_path: impl AsRef<std::path::Path>,
194    ) -> eyre::Result<()>
195    where
196        ChainSpec: EthChainSpec + reth_chainspec::EthereumHardforks,
197    {
198        let mut should_save = reth_config.prune.segments.migrate();
199
200        if let Some(prune_config) = config.prune_config() {
201            if reth_config.prune != prune_config {
202                reth_config.set_prune_config(prune_config);
203                should_save = true;
204            }
205        } else if !reth_config.prune.is_default() {
206            info!(target: "reth::cli", "Pruning configuration is present in the config file, but no CLI arguments are provided. Using config from file.");
207        }
208
209        if should_save {
210            info!(target: "reth::cli", "Saving prune config to toml file");
211            reth_config.save(config_path.as_ref())?;
212        }
213
214        Ok(())
215    }
216
217    /// Convenience function to [`Self::configure_globals`]
218    pub fn with_configured_globals(self, reserved_cpu_cores: usize) -> Self {
219        self.configure_globals(reserved_cpu_cores);
220        self
221    }
222
223    /// Configure global settings this includes:
224    ///
225    /// - Raising the file descriptor limit
226    /// - Configuring the global rayon thread pool for implicit `par_iter` usage
227    pub fn configure_globals(&self, reserved_cpu_cores: usize) {
228        // Raise the fd limit of the process.
229        // Does not do anything on windows.
230        match fdlimit::raise_fd_limit() {
231            Ok(fdlimit::Outcome::LimitRaised { from, to }) => {
232                debug!(from, to, "Raised file descriptor limit");
233            }
234            Ok(fdlimit::Outcome::Unsupported) => {}
235            Err(err) => warn!(%err, "Failed to raise file descriptor limit"),
236        }
237
238        // Configure the implicit global rayon pool for `par_iter` usage.
239        // TODO: reserved_cpu_cores is currently ignored because subtracting from thread pool
240        // sizes doesn't actually reserve CPU cores for other processes.
241        let _ = reserved_cpu_cores;
242        let num_threads = available_parallelism().map_or(1, NonZeroUsize::get);
243        if let Err(err) = ThreadPoolBuilder::new()
244            .num_threads(num_threads)
245            .thread_name(|i| format!("rayon-{i:02}"))
246            .build_global()
247        {
248            warn!(%err, "Failed to build global thread pool")
249        }
250    }
251}
252
253/// A [`LaunchContext`] along with an additional value.
254///
255/// The type parameter `T` represents the current state of the launch process.
256/// Methods are conditionally implemented based on `T`, ensuring operations
257/// are only available when their prerequisites are met.
258///
259/// For example:
260/// - Config methods when `T = WithConfigs<ChainSpec>`
261/// - Database operations when `T = Attached<WithConfigs<ChainSpec>, DB>`
262/// - Provider operations when `T = Attached<WithConfigs<ChainSpec>, ProviderFactory<N>>`
263#[derive(Debug, Clone)]
264pub struct LaunchContextWith<T> {
265    /// The wrapped launch context.
266    pub inner: LaunchContext,
267    /// The additional attached value.
268    pub attachment: T,
269}
270
271impl<T> LaunchContextWith<T> {
272    /// Configure global settings this includes:
273    ///
274    /// - Raising the file descriptor limit
275    /// - Configuring the global rayon thread pool
276    pub fn configure_globals(&self, reserved_cpu_cores: u64) {
277        self.inner.configure_globals(reserved_cpu_cores.try_into().unwrap());
278    }
279
280    /// Returns the data directory.
281    pub const fn data_dir(&self) -> &ChainPath<DataDirPath> {
282        &self.inner.data_dir
283    }
284
285    /// Returns the task executor.
286    pub const fn task_executor(&self) -> &TaskExecutor {
287        &self.inner.task_executor
288    }
289
290    /// Attaches another value to the launch context.
291    pub fn attach<A>(self, attachment: A) -> LaunchContextWith<Attached<T, A>> {
292        LaunchContextWith {
293            inner: self.inner,
294            attachment: Attached::new(self.attachment, attachment),
295        }
296    }
297
298    /// Consumes the type and calls a function with a reference to the context.
299    // Returns the context again
300    pub fn inspect<F>(self, f: F) -> Self
301    where
302        F: FnOnce(&Self),
303    {
304        f(&self);
305        self
306    }
307}
308
309impl<ChainSpec> LaunchContextWith<WithConfigs<ChainSpec>> {
310    /// Resolves the trusted peers and adds them to the toml config.
311    pub fn with_resolved_peers(mut self) -> eyre::Result<Self> {
312        if !self.attachment.config.network.trusted_peers.is_empty() {
313            info!(target: "reth::cli", "Adding trusted nodes");
314
315            self.attachment
316                .toml_config
317                .peers
318                .trusted_nodes
319                .extend(self.attachment.config.network.trusted_peers.clone());
320        }
321        Ok(self)
322    }
323}
324
325impl<L, R> LaunchContextWith<Attached<L, R>> {
326    /// Get a reference to the left value.
327    pub const fn left(&self) -> &L {
328        &self.attachment.left
329    }
330
331    /// Get a reference to the right value.
332    pub const fn right(&self) -> &R {
333        &self.attachment.right
334    }
335
336    /// Get a mutable reference to the left value.
337    pub const fn left_mut(&mut self) -> &mut L {
338        &mut self.attachment.left
339    }
340
341    /// Get a mutable reference to the right value.
342    pub const fn right_mut(&mut self) -> &mut R {
343        &mut self.attachment.right
344    }
345}
346impl<R, ChainSpec: EthChainSpec> LaunchContextWith<Attached<WithConfigs<ChainSpec>, R>> {
347    /// Adjust certain settings in the config to make sure they are set correctly
348    ///
349    /// This includes:
350    /// - Making sure the ETL dir is set to the datadir
351    /// - RPC settings are adjusted to the correct port
352    pub fn with_adjusted_configs(self) -> Self {
353        self.ensure_etl_datadir().with_adjusted_instance_ports()
354    }
355
356    /// Make sure ETL doesn't default to /tmp/, but to whatever datadir is set to
357    pub fn ensure_etl_datadir(mut self) -> Self {
358        if self.toml_config_mut().stages.etl.dir.is_none() {
359            let etl_path = EtlConfig::from_datadir(self.data_dir().data_dir());
360            if etl_path.exists() {
361                // Remove etl-path files on launch
362                if let Err(err) = fs::remove_dir_all(&etl_path) {
363                    warn!(target: "reth::cli", ?etl_path, %err, "Failed to remove ETL path on launch");
364                }
365            }
366            self.toml_config_mut().stages.etl.dir = Some(etl_path);
367        }
368
369        self
370    }
371
372    /// Change rpc port numbers based on the instance number.
373    pub fn with_adjusted_instance_ports(mut self) -> Self {
374        self.node_config_mut().adjust_instance_ports();
375        self
376    }
377
378    /// Returns the container for all config types
379    pub const fn configs(&self) -> &WithConfigs<ChainSpec> {
380        self.attachment.left()
381    }
382
383    /// Returns the attached [`NodeConfig`].
384    pub const fn node_config(&self) -> &NodeConfig<ChainSpec> {
385        &self.left().config
386    }
387
388    /// Returns the attached [`NodeConfig`].
389    pub const fn node_config_mut(&mut self) -> &mut NodeConfig<ChainSpec> {
390        &mut self.left_mut().config
391    }
392
393    /// Returns the attached toml config [`reth_config::Config`].
394    pub const fn toml_config(&self) -> &reth_config::Config {
395        &self.left().toml_config
396    }
397
398    /// Returns the attached toml config [`reth_config::Config`].
399    pub const fn toml_config_mut(&mut self) -> &mut reth_config::Config {
400        &mut self.left_mut().toml_config
401    }
402
403    /// Returns the configured chain spec.
404    pub fn chain_spec(&self) -> Arc<ChainSpec> {
405        self.node_config().chain.clone()
406    }
407
408    /// Get the hash of the genesis block.
409    pub fn genesis_hash(&self) -> B256 {
410        self.node_config().chain.genesis_hash()
411    }
412
413    /// Returns the chain identifier of the node.
414    pub fn chain_id(&self) -> Chain {
415        self.node_config().chain.chain()
416    }
417
418    /// Returns true if the node is configured as --dev
419    pub const fn is_dev(&self) -> bool {
420        self.node_config().dev.dev
421    }
422
423    /// Returns the configured [`PruneConfig`]
424    ///
425    /// Any configuration set in CLI will take precedence over those set in toml
426    pub fn prune_config(&self) -> PruneConfig
427    where
428        ChainSpec: reth_chainspec::EthereumHardforks,
429    {
430        let Some(mut node_prune_config) = self.node_config().prune_config() else {
431            // No CLI config is set, use the toml config.
432            return self.toml_config().prune.clone();
433        };
434
435        // Otherwise, use the CLI configuration and merge with toml config.
436        node_prune_config.merge(self.toml_config().prune.clone());
437        node_prune_config
438    }
439
440    /// Returns the configured [`PruneModes`], returning the default if no config was available.
441    pub fn prune_modes(&self) -> PruneModes
442    where
443        ChainSpec: reth_chainspec::EthereumHardforks,
444    {
445        self.prune_config().segments
446    }
447
448    /// Returns an initialized [`PrunerBuilder`] based on the configured [`PruneConfig`]
449    pub fn pruner_builder(&self) -> PrunerBuilder
450    where
451        ChainSpec: reth_chainspec::EthereumHardforks,
452    {
453        PrunerBuilder::new(self.prune_config())
454    }
455
456    /// Loads the JWT secret for the engine API
457    pub fn auth_jwt_secret(&self) -> eyre::Result<JwtSecret> {
458        let default_jwt_path = self.data_dir().jwt();
459        let secret = self.node_config().rpc.auth_jwt_secret(default_jwt_path)?;
460        Ok(secret)
461    }
462
463    /// Returns the [`MiningMode`] intended for --dev mode.
464    pub fn dev_mining_mode<Pool>(&self, pool: Pool) -> MiningMode<Pool>
465    where
466        Pool: TransactionPool + Unpin,
467    {
468        self.node_config().dev_mining_mode(pool)
469    }
470}
471
472impl<DB, ChainSpec> LaunchContextWith<Attached<WithConfigs<ChainSpec>, DB>>
473where
474    DB: Database + Clone + 'static,
475    ChainSpec: EthChainSpec + EthereumHardforks + 'static,
476{
477    /// Returns the [`ProviderFactory`] for the attached storage after executing a consistent check
478    /// between the database and static files. **It may execute a pipeline unwind if it fails this
479    /// check.**
480    pub async fn create_provider_factory<N, Evm>(
481        &self,
482        overlay_manager: OverlayManager<N::Primitives>,
483        rocksdb_provider: Option<RocksDBProvider>,
484        disabled_stages: &[StageId],
485    ) -> eyre::Result<ProviderFactory<N>>
486    where
487        N: ProviderNodeTypes<DB = DB, ChainSpec = ChainSpec>,
488        Evm: ConfigureEvm<Primitives = N::Primitives> + 'static,
489    {
490        // Validate static files configuration
491        let static_files_config = &self.toml_config().static_files;
492        static_files_config.validate()?;
493
494        let prune_config = self.prune_config();
495
496        let mut blocks_per_file = static_files_config.as_blocks_per_file_map();
497        // Receipts in static files are pruned by deleting whole files, so with the default file
498        // size a distance-based prune target is only reached every 500k blocks. Unless a file size
499        // is explicitly configured, derive one from the prune distance so retention tracks the
500        // configured distance.
501        if blocks_per_file.get(StaticFileSegment::Receipts).is_none() &&
502            let Some(PruneMode::Distance(distance)) = prune_config.segments.receipts
503        {
504            blocks_per_file
505                .insert(StaticFileSegment::Receipts, blocks_per_file_for_prune_distance(distance));
506        }
507
508        // Apply per-segment blocks_per_file configuration
509        let static_file_provider =
510            StaticFileProviderBuilder::read_write(self.data_dir().static_files())
511                .with_metrics()
512                .with_blocks_per_file_for_segments(&blocks_per_file)
513                .with_genesis_block_number(self.chain_spec().genesis().number.unwrap_or_default())
514                .build()?;
515
516        // Use the provided RocksDB provider or create a new one
517        let rocksdb_provider = if let Some(provider) = rocksdb_provider {
518            provider
519        } else {
520            RocksDBProvider::builder(self.data_dir().rocksdb())
521                .with_default_tables()
522                .with_metrics()
523                .with_statistics()
524                .build()?
525        };
526
527        let bal_store = self
528            .node_config()
529            .db
530            .balstore_cache_size
531            .map(|distance| {
532                RocksDBBalStore::with_buffer_retention_distance(rocksdb_provider.clone(), distance)
533            })
534            .unwrap_or_else(|| RocksDBBalStore::new(rocksdb_provider.clone()));
535        let bal_store = BalStoreHandle::new(bal_store);
536        let factory = ProviderFactory::new(
537            self.right().clone(),
538            self.chain_spec(),
539            static_file_provider,
540            rocksdb_provider,
541            self.task_executor().clone(),
542        )?
543        .with_prune_modes(prune_config.segments)
544        .with_minimum_pruning_distance(prune_config.minimum_pruning_distance)
545        .with_overlay_manager(overlay_manager)
546        .with_bal_store(bal_store);
547
548        // Check consistency between the database and static files, returning
549        // the unwind targets for each storage layer if inconsistencies are
550        // found.
551        let (rocksdb_unwind, static_file_unwind) = factory.check_consistency()?;
552        let provider_ro = factory.database_provider_ro()?;
553        // Finish is committed before Merkle during unwind, so this marker is authoritative when
554        // resuming an interrupted partial trie unwind.
555        let (partial_trie_unwind, has_persisted_partial_trie_unwind) =
556            get_partial_trie_unwind_marker(&provider_ro)?;
557        drop(provider_ro);
558        let persist_partial_trie_unwind =
559            !has_persisted_partial_trie_unwind && partial_trie_unwind.is_some();
560        let partial_trie_unwind_target =
561            partial_trie_unwind.map(|marker| marker.partial_state_trie);
562        // Recover the partial state trie first. Its unwind enables
563        // `walk_all_changed_branch_children`, which is more expensive than a normal unwind, so
564        // it only runs to the partial trie target. A lower storage-layer target is then unwound
565        // normally.
566        let storage_unwind = [rocksdb_unwind, static_file_unwind].into_iter().flatten().min();
567        let storage_unwind = storage_unwind.filter(|unwind_block| {
568            partial_trie_unwind_target.is_none_or(|partial_trie| *unwind_block < partial_trie)
569        });
570
571        if partial_trie_unwind_target.is_some() || storage_unwind.is_some() {
572            let build_unwind_pipeline = |walk_all_changed_branch_children| {
573                let (_tip_tx, tip_rx) = watch::channel(B256::ZERO);
574                let mut stages = DefaultStages::new(
575                    factory.clone(),
576                    tip_rx,
577                    Arc::new(NoopConsensus::default()),
578                    NoopHeaderDownloader::default(),
579                    NoopBodiesDownloader::default(),
580                    NoopEvmConfig::<Evm>::default(),
581                    self.toml_config().stages.clone(),
582                    self.prune_modes(),
583                    None,
584                )
585                .builder()
586                .disable_all(disabled_stages);
587
588                if walk_all_changed_branch_children {
589                    // Partial trie recovery is not complete until Merkle has unwound.
590                    stages =
591                        stages.set(MerkleStage::new_unwind(true)).enable(StageId::MerkleUnwind);
592                }
593
594                PipelineBuilder::default().add_stages(stages).build(
595                    factory.clone(),
596                    StaticFileProducer::new(factory.clone(), self.prune_modes()),
597                )
598            };
599            let mut unwinds = Vec::with_capacity(2);
600
601            if let Some(unwind_block) = partial_trie_unwind_target {
602                unwinds.push((
603                    PipelineTarget::Unwind(unwind_block),
604                    "partial state trie".to_owned(),
605                    build_unwind_pipeline(true),
606                    true,
607                ));
608            }
609
610            if let Some(unwind_block) = storage_unwind {
611                // Highly unlikely to happen, and given its destructive nature, it's better to
612                // panic instead. Unwinding to 0 would leave MDBX with a huge free list size.
613                let inconsistency_source = match (rocksdb_unwind, static_file_unwind) {
614                    (Some(_), Some(_)) => "RocksDB and static file",
615                    (Some(_), None) => "RocksDB",
616                    (None, Some(_)) => "static file",
617                    (None, None) => unreachable!(),
618                };
619                assert_ne!(
620                    unwind_block, 0,
621                    "A {inconsistency_source} inconsistency was found that would trigger an unwind to block 0"
622                );
623                unwinds.push((
624                    PipelineTarget::Unwind(unwind_block),
625                    inconsistency_source.to_owned(),
626                    build_unwind_pipeline(false),
627                    false,
628                ));
629            }
630
631            if persist_partial_trie_unwind {
632                // The marker must be durable before any unwind stage can commit.
633                let provider_rw = factory.database_provider_rw()?;
634                write_partial_trie_unwind_marker(
635                    &provider_rw,
636                    partial_trie_unwind.expect("partial trie unwind marker must exist"),
637                )?;
638                provider_rw.commit()?;
639            }
640
641            let (tx, rx) = oneshot::channel();
642            let factory = factory.clone();
643
644            // Pipeline should be run as blocking and panic if it fails.
645            self.task_executor().spawn_critical_blocking_task("pipeline task", async move {
646                let result: Result<(), reth_stages::PipelineError> = async {
647                    for (unwind_target, inconsistency_source, pipeline, clear_partial_trie_unwind) in
648                        unwinds
649                    {
650                        info!(target: "reth::cli", %unwind_target, %inconsistency_source, "Executing unwind after consistency check.");
651                        let (_, result) = pipeline.run_as_fut(Some(unwind_target)).await;
652                        result.inspect_err(|err| {
653                            error!(target: "reth::cli", %unwind_target, %inconsistency_source, %err, "failed to run unwind");
654                        })?;
655
656                        if clear_partial_trie_unwind {
657                            let provider_rw = factory.database_provider_rw()?;
658                            delete_partial_trie_unwind_marker(&provider_rw)?;
659                            provider_rw.commit()?;
660                        }
661                    }
662                    Ok(())
663                }
664                .await;
665                let _ = tx.send(result);
666            });
667            rx.await??;
668        }
669
670        Ok(factory)
671    }
672
673    /// Creates a new [`ProviderFactory`] and attaches it to the launch context.
674    pub async fn with_provider_factory<N, Evm>(
675        self,
676        overlay_manager: OverlayManager<N::Primitives>,
677        rocksdb_provider: Option<RocksDBProvider>,
678        disabled_stages: &[StageId],
679    ) -> eyre::Result<LaunchContextWith<Attached<WithConfigs<ChainSpec>, ProviderFactory<N>>>>
680    where
681        N: ProviderNodeTypes<DB = DB, ChainSpec = ChainSpec>,
682        Evm: ConfigureEvm<Primitives = N::Primitives> + 'static,
683    {
684        let factory = self
685            .create_provider_factory::<N, Evm>(overlay_manager, rocksdb_provider, disabled_stages)
686            .await?;
687        let ctx = LaunchContextWith {
688            inner: self.inner,
689            attachment: self.attachment.map_right(|_| factory),
690        };
691
692        Ok(ctx)
693    }
694}
695
696impl<T> LaunchContextWith<Attached<WithConfigs<T::ChainSpec>, ProviderFactory<T>>>
697where
698    T: ProviderNodeTypes,
699{
700    /// Returns access to the underlying database.
701    pub const fn database(&self) -> &T::DB {
702        self.right().db_ref()
703    }
704
705    /// Returns the configured `ProviderFactory`.
706    pub const fn provider_factory(&self) -> &ProviderFactory<T> {
707        self.right()
708    }
709
710    /// Returns the static file provider to interact with the static files.
711    pub fn static_file_provider(&self) -> StaticFileProvider<T::Primitives> {
712        self.right().static_file_provider()
713    }
714
715    /// This launches the prometheus endpoint.
716    ///
717    /// Convenience function to [`Self::start_prometheus_endpoint`]
718    pub async fn with_prometheus_server(self) -> eyre::Result<Self>
719    where
720        T::ChainSpec: EthereumHardforks,
721    {
722        self.start_prometheus_endpoint().await?;
723        Ok(self)
724    }
725
726    /// Starts the prometheus endpoint.
727    pub async fn start_prometheus_endpoint(&self) -> eyre::Result<()>
728    where
729        T::ChainSpec: EthereumHardforks,
730    {
731        // ensure recorder runs upkeep periodically
732        install_prometheus_recorder().spawn_upkeep();
733
734        let listen_addr = self.node_config().metrics.prometheus;
735        if let Some(addr) = listen_addr {
736            let prune_config = self.prune_config();
737            let pruning_mode =
738                PruneConfigKind::from_config(&prune_config, self.chain_spec().as_ref()).as_str();
739            // On existing databases, stored settings are authoritative and already cached by the
740            // provider factory. Fresh databases do not have storage metadata until genesis is
741            // initialized, so report the configured setting during this pre-genesis startup window.
742            let storage_settings =
743                if self.provider_factory().get_stage_checkpoint(StageId::Headers)?.is_some() {
744                    self.provider_factory().cached_storage_settings()
745                } else {
746                    self.node_config().storage_settings()
747                };
748            let config = MetricServerConfig::new(
749                addr,
750                VersionInfo {
751                    version: version_metadata().cargo_pkg_version.as_ref(),
752                    build_timestamp: version_metadata().vergen_build_timestamp.as_ref(),
753                    cargo_features: version_metadata().vergen_cargo_features.as_ref(),
754                    git_sha: version_metadata().vergen_git_sha.as_ref(),
755                    target_triple: version_metadata().vergen_cargo_target_triple.as_ref(),
756                    build_profile: version_metadata().build_profile_name.as_ref(),
757                },
758                ChainSpecInfo { name: self.chain_id().to_string() },
759                self.task_executor().clone(),
760                metrics_hooks(self.provider_factory()),
761                self.data_dir().pprof_dumps(),
762            )
763            .with_storage_settings_info(StorageSettingsInfo {
764                storage_v2: storage_settings.storage_v2,
765                pruning_mode,
766                prune_config: serde_json::to_string(&prune_config)
767                    .expect("serializing PruneConfig should not fail"),
768            })
769            .with_push_gateway(
770                self.node_config().metrics.push_gateway_url.clone(),
771                self.node_config().metrics.push_gateway_interval,
772            );
773
774            MetricServer::new(config).serve().await?;
775        }
776
777        Ok(())
778    }
779
780    /// Convenience function to [`Self::init_genesis`]
781    pub fn with_genesis(self) -> Result<Self, InitStorageError> {
782        init_genesis_with_settings_and_validate(
783            self.provider_factory(),
784            self.node_config().storage_settings(),
785            !self.node_config().debug.skip_genesis_validation,
786        )?;
787        Ok(self)
788    }
789
790    /// Write the genesis block and state if it has not already been written
791    pub fn init_genesis(&self) -> Result<B256, InitStorageError> {
792        init_genesis_with_settings(self.provider_factory(), self.node_config().storage_settings())
793    }
794
795    /// Creates a new `WithMeteredProvider` container and attaches it to the
796    /// launch context.
797    ///
798    /// This spawns a metrics task that listens for metrics related events and updates metrics for
799    /// prometheus.
800    pub fn with_metrics_task(
801        self,
802    ) -> LaunchContextWith<Attached<WithConfigs<T::ChainSpec>, WithMeteredProvider<T>>> {
803        let (metrics_sender, metrics_receiver) = unbounded_channel();
804
805        let with_metrics =
806            WithMeteredProvider { provider_factory: self.right().clone(), metrics_sender };
807
808        debug!(target: "reth::cli", "Spawning stages metrics listener task");
809        let sync_metrics_listener = reth_stages::MetricsListener::new(metrics_receiver);
810        self.task_executor()
811            .spawn_critical_task("stages metrics listener task", sync_metrics_listener);
812
813        LaunchContextWith {
814            inner: self.inner,
815            attachment: self.attachment.map_right(|_| with_metrics),
816        }
817    }
818}
819
820impl<N, DB>
821    LaunchContextWith<
822        Attached<WithConfigs<N::ChainSpec>, WithMeteredProvider<NodeTypesWithDBAdapter<N, DB>>>,
823    >
824where
825    N: NodeTypes,
826    DB: Database + DatabaseMetrics + Clone + Unpin + 'static,
827{
828    /// Returns the configured `ProviderFactory`.
829    const fn provider_factory(&self) -> &ProviderFactory<NodeTypesWithDBAdapter<N, DB>> {
830        &self.right().provider_factory
831    }
832
833    /// Returns the metrics sender.
834    fn sync_metrics_tx(&self) -> UnboundedSender<MetricEvent> {
835        self.right().metrics_sender.clone()
836    }
837
838    /// Creates a `BlockchainProvider` and attaches it to the launch context.
839    #[expect(clippy::complexity)]
840    pub fn with_blockchain_db<T, F>(
841        self,
842        create_blockchain_provider: F,
843    ) -> eyre::Result<LaunchContextWith<Attached<WithConfigs<N::ChainSpec>, WithMeteredProviders<T>>>>
844    where
845        T: FullNodeTypes<Types = N, DB = DB>,
846        F: FnOnce(ProviderFactory<NodeTypesWithDBAdapter<N, DB>>) -> eyre::Result<T::Provider>,
847    {
848        let blockchain_db = create_blockchain_provider(self.provider_factory().clone())?;
849
850        let metered_providers = WithMeteredProviders {
851            db_provider_container: WithMeteredProvider {
852                provider_factory: self.provider_factory().clone(),
853                metrics_sender: self.sync_metrics_tx(),
854            },
855            blockchain_db,
856        };
857
858        let ctx = LaunchContextWith {
859            inner: self.inner,
860            attachment: self.attachment.map_right(|_| metered_providers),
861        };
862
863        Ok(ctx)
864    }
865}
866
867impl<T>
868    LaunchContextWith<
869        Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, WithMeteredProviders<T>>,
870    >
871where
872    T: FullNodeTypes<Types: NodeTypesForProvider>,
873{
874    /// Returns access to the underlying database.
875    pub const fn database(&self) -> &T::DB {
876        self.provider_factory().db_ref()
877    }
878
879    /// Returns the configured `ProviderFactory`.
880    pub const fn provider_factory(
881        &self,
882    ) -> &ProviderFactory<NodeTypesWithDBAdapter<T::Types, T::DB>> {
883        &self.right().db_provider_container.provider_factory
884    }
885
886    /// Fetches the head block from the database.
887    ///
888    /// If the database is empty, returns the genesis block.
889    pub fn lookup_head(&self) -> eyre::Result<Head> {
890        self.node_config()
891            .lookup_head(self.provider_factory())
892            .wrap_err("the head block is missing")
893    }
894
895    /// Returns the metrics sender.
896    pub fn sync_metrics_tx(&self) -> UnboundedSender<MetricEvent> {
897        self.right().db_provider_container.metrics_sender.clone()
898    }
899
900    /// Returns a reference to the blockchain provider.
901    pub const fn blockchain_db(&self) -> &T::Provider {
902        &self.right().blockchain_db
903    }
904
905    /// Creates a `NodeAdapter` and attaches it to the launch context.
906    pub async fn with_components<CB>(
907        self,
908        components_builder: CB,
909        on_component_initialized: Box<
910            dyn OnComponentInitializedHook<NodeAdapter<T, CB::Components>>,
911        >,
912    ) -> eyre::Result<
913        LaunchContextWith<
914            Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, WithComponents<T, CB>>,
915        >,
916    >
917    where
918        CB: NodeComponentsBuilder<T>,
919    {
920        // fetch the head block from the database
921        let head = self.lookup_head()?;
922
923        let builder_ctx = BuilderContext::new(
924            head,
925            self.blockchain_db().clone(),
926            self.task_executor().clone(),
927            self.configs().clone(),
928        );
929
930        debug!(target: "reth::cli", "creating components");
931        let components = components_builder.build_components(&builder_ctx).await?;
932
933        let blockchain_db = self.blockchain_db().clone();
934
935        let node_adapter = NodeAdapter {
936            components,
937            task_executor: self.task_executor().clone(),
938            provider: blockchain_db,
939        };
940
941        debug!(target: "reth::cli", "calling on_component_initialized hook");
942        on_component_initialized.on_event(node_adapter.clone())?;
943
944        let components_container = WithComponents {
945            db_provider_container: WithMeteredProvider {
946                provider_factory: self.provider_factory().clone(),
947                metrics_sender: self.sync_metrics_tx(),
948            },
949            node_adapter,
950            head,
951        };
952
953        let ctx = LaunchContextWith {
954            inner: self.inner,
955            attachment: self.attachment.map_right(|_| components_container),
956        };
957
958        Ok(ctx)
959    }
960}
961
962impl<T, CB>
963    LaunchContextWith<
964        Attached<WithConfigs<<T::Types as NodeTypes>::ChainSpec>, WithComponents<T, CB>>,
965    >
966where
967    T: FullNodeTypes<Types: NodeTypesForProvider>,
968    CB: NodeComponentsBuilder<T>,
969{
970    /// Returns the configured `ProviderFactory`.
971    pub const fn provider_factory(
972        &self,
973    ) -> &ProviderFactory<NodeTypesWithDBAdapter<T::Types, T::DB>> {
974        &self.right().db_provider_container.provider_factory
975    }
976
977    /// Returns the max block that the node should run to, looking it up from the network if
978    /// necessary
979    pub async fn max_block<C>(&self, client: C) -> eyre::Result<Option<BlockNumber>>
980    where
981        C: HeadersClient<Header: BlockHeader>,
982    {
983        self.node_config().max_block(client, self.provider_factory().clone()).await
984    }
985
986    /// Returns the static file provider to interact with the static files.
987    pub fn static_file_provider(&self) -> StaticFileProvider<<T::Types as NodeTypes>::Primitives> {
988        self.provider_factory().static_file_provider()
989    }
990
991    /// Creates a new [`StaticFileProducer`] with the attached database.
992    pub fn static_file_producer(
993        &self,
994    ) -> StaticFileProducer<ProviderFactory<NodeTypesWithDBAdapter<T::Types, T::DB>>> {
995        StaticFileProducer::new(self.provider_factory().clone(), self.prune_modes())
996    }
997
998    /// Returns the current head block.
999    pub const fn head(&self) -> Head {
1000        self.right().head
1001    }
1002
1003    /// Returns the configured `NodeAdapter`.
1004    pub const fn node_adapter(&self) -> &NodeAdapter<T, CB::Components> {
1005        &self.right().node_adapter
1006    }
1007
1008    /// Returns mutable reference to the configured `NodeAdapter`.
1009    pub const fn node_adapter_mut(&mut self) -> &mut NodeAdapter<T, CB::Components> {
1010        &mut self.right_mut().node_adapter
1011    }
1012
1013    /// Returns a reference to the blockchain provider.
1014    pub const fn blockchain_db(&self) -> &T::Provider {
1015        &self.node_adapter().provider
1016    }
1017
1018    /// Returns the initial backfill to sync to at launch.
1019    ///
1020    /// This returns the configured `debug.tip` if set, otherwise it will check if backfill was
1021    /// previously interrupted and returns the block hash of the last checkpoint, see also
1022    /// [`Self::check_pipeline_consistency`]
1023    pub fn initial_backfill_target(
1024        &self,
1025        disabled_stages: &[StageId],
1026    ) -> ProviderResult<Option<B256>> {
1027        let mut initial_target = self.node_config().debug.tip;
1028
1029        if initial_target.is_none() {
1030            initial_target = self.check_pipeline_consistency(disabled_stages)?;
1031        }
1032
1033        Ok(initial_target)
1034    }
1035
1036    /// Returns true if the node should terminate after the initial backfill run.
1037    ///
1038    /// This is the case if any of these configs are set:
1039    ///  `--debug.max-block`
1040    ///  `--debug.terminate`
1041    pub const fn terminate_after_initial_backfill(&self) -> bool {
1042        self.node_config().debug.terminate || self.node_config().debug.max_block.is_some()
1043    }
1044
1045    /// Ensures that the database matches chain-specific requirements.
1046    ///
1047    /// This checks for OP-Mainnet and ensures we have all the necessary data to progress (past
1048    /// bedrock height)
1049    fn ensure_chain_specific_db_checks(&self) -> ProviderResult<()> {
1050        if self.chain_spec().is_optimism() &&
1051            !self.is_dev() &&
1052            self.chain_id() == Chain::optimism_mainnet()
1053        {
1054            let latest = self.blockchain_db().last_block_number()?;
1055            // bedrock height
1056            if latest < 105235063 {
1057                error!(
1058                    "Op-mainnet has been launched without importing the pre-Bedrock state. The chain can't progress without this. See also https://reth.rs/run/sync-op-mainnet.html?minimal-bootstrap-recommended"
1059                );
1060                return Err(ProviderError::BestBlockNotFound);
1061            }
1062        }
1063
1064        Ok(())
1065    }
1066
1067    /// Check if the pipeline is consistent (all stages have the checkpoint block numbers no less
1068    /// than the checkpoint of the first stage).
1069    ///
1070    /// This will return the pipeline target if:
1071    ///  * the pipeline was interrupted during its previous run
1072    ///  * a new stage was added
1073    ///  * stage data was dropped manually through `reth stage drop ...`
1074    ///
1075    /// # Returns
1076    ///
1077    /// A target block hash if the pipeline is inconsistent, otherwise `None`.
1078    pub fn check_pipeline_consistency(
1079        &self,
1080        disabled_stages: &[StageId],
1081    ) -> ProviderResult<Option<B256>> {
1082        // We skip the era stage if it's not enabled
1083        let era_enabled = self.era_import_source().is_some();
1084        let mut all_stages = StageId::ALL
1085            .into_iter()
1086            .filter(|id| (era_enabled || id != &StageId::Era) && !disabled_stages.contains(id));
1087
1088        // Get the expected first stage based on config.
1089        let first_stage = all_stages.next().expect("there must be at least one stage");
1090
1091        // If no target was provided, check if the stages are congruent - check if the
1092        // checkpoint of the last stage matches the checkpoint of the first.
1093        let first_stage_checkpoint = self
1094            .blockchain_db()
1095            .get_stage_checkpoint(first_stage)?
1096            .unwrap_or_default()
1097            .block_number;
1098
1099        // Compare all other stages against the first
1100        for stage_id in all_stages {
1101            let stage_checkpoint = self
1102                .blockchain_db()
1103                .get_stage_checkpoint(stage_id)?
1104                .unwrap_or_default()
1105                .block_number;
1106
1107            // If the checkpoint of any stage is less than the checkpoint of the first stage,
1108            // retrieve and return the block hash of the latest header and use it as the target.
1109            debug!(
1110                target: "consensus::engine",
1111                first_stage_id = %first_stage,
1112                first_stage_checkpoint,
1113                stage_id = %stage_id,
1114                stage_checkpoint = stage_checkpoint,
1115                "Checking stage against first stage",
1116            );
1117            if stage_checkpoint < first_stage_checkpoint {
1118                debug!(
1119                    target: "consensus::engine",
1120                    first_stage_id = %first_stage,
1121                    first_stage_checkpoint,
1122                    inconsistent_stage_id = %stage_id,
1123                    inconsistent_stage_checkpoint = stage_checkpoint,
1124                    "Pipeline sync progress is inconsistent"
1125                );
1126                return self.blockchain_db().block_hash(first_stage_checkpoint);
1127            }
1128        }
1129
1130        self.ensure_chain_specific_db_checks()?;
1131
1132        Ok(None)
1133    }
1134
1135    /// Returns the metrics sender.
1136    pub fn sync_metrics_tx(&self) -> UnboundedSender<MetricEvent> {
1137        self.right().db_provider_container.metrics_sender.clone()
1138    }
1139
1140    /// Returns the node adapter components.
1141    pub const fn components(&self) -> &CB::Components {
1142        &self.node_adapter().components
1143    }
1144
1145    /// Launches ExEx (Execution Extensions) and returns the ExEx manager handle.
1146    #[expect(clippy::type_complexity)]
1147    pub async fn launch_exex(
1148        &self,
1149        installed_exex: Vec<(
1150            String,
1151            Box<dyn crate::exex::BoxedLaunchExEx<NodeAdapter<T, CB::Components>>>,
1152        )>,
1153    ) -> eyre::Result<Option<ExExManagerHandle<PrimitivesTy<T::Types>>>> {
1154        self.exex_launcher(installed_exex).launch().await
1155    }
1156
1157    /// Creates an [`ExExLauncher`] for the installed ExExes.
1158    ///
1159    /// This returns the launcher before calling `.launch()`, allowing custom configuration
1160    /// such as setting the WAL blocks warning threshold for L2 chains with faster block times:
1161    ///
1162    /// ```ignore
1163    /// ctx.exex_launcher(exexes)
1164    ///     .with_wal_blocks_warning(768)  // For 2-second block times
1165    ///     .launch()
1166    ///     .await
1167    /// ```
1168    #[expect(clippy::type_complexity)]
1169    pub fn exex_launcher(
1170        &self,
1171        installed_exex: Vec<(
1172            String,
1173            Box<dyn crate::exex::BoxedLaunchExEx<NodeAdapter<T, CB::Components>>>,
1174        )>,
1175    ) -> ExExLauncher<NodeAdapter<T, CB::Components>> {
1176        ExExLauncher::new(
1177            self.head(),
1178            self.node_adapter().clone(),
1179            installed_exex,
1180            self.configs().clone(),
1181        )
1182    }
1183
1184    /// Creates the ERA import source based on node configuration.
1185    ///
1186    /// Returns `Some(EraImportSource)` if ERA is enabled in the node config, otherwise `None`.
1187    pub fn era_import_source(&self) -> Option<EraImportSource> {
1188        let node_config = self.node_config();
1189        if !node_config.era.enabled {
1190            return None;
1191        }
1192
1193        EraImportSource::maybe_new(
1194            node_config.era.source.path.clone(),
1195            node_config.era.source.url.clone(),
1196            || node_config.chain.chain().kind().default_era_host(),
1197            || node_config.datadir().data_dir().join("era").into(),
1198        )
1199    }
1200
1201    /// Creates consensus layer health events stream based on node configuration.
1202    ///
1203    /// Returns a stream that monitors consensus layer health if:
1204    /// - No debug tip is configured
1205    /// - Not running in dev mode
1206    ///
1207    /// Otherwise returns an empty stream.
1208    pub fn consensus_layer_events(
1209        &self,
1210    ) -> impl Stream<Item = NodeEvent<PrimitivesTy<T::Types>>> + 'static
1211    where
1212        T::Provider: reth_provider::CanonChainTracker,
1213    {
1214        if self.node_config().debug.tip.is_none() && !self.is_dev() {
1215            Either::Left(
1216                ConsensusLayerHealthEvents::new(Box::new(self.blockchain_db().clone()))
1217                    .map(Into::into),
1218            )
1219        } else {
1220            Either::Right(stream::empty())
1221        }
1222    }
1223
1224    /// Spawns the [`EthStatsService`] service if configured.
1225    pub async fn spawn_ethstats<St>(&self, mut engine_events: St) -> eyre::Result<()>
1226    where
1227        St: Stream<Item = reth_engine_primitives::ConsensusEngineEvent<PrimitivesTy<T::Types>>>
1228            + Send
1229            + Unpin
1230            + 'static,
1231    {
1232        let Some(url) = self.node_config().debug.ethstats.as_ref() else { return Ok(()) };
1233
1234        let network = self.components().network().clone();
1235        let pool = self.components().pool().clone();
1236        let provider = self.node_adapter().provider.clone();
1237
1238        info!(target: "reth::cli", "Starting EthStats service at {}", url);
1239
1240        let ethstats = EthStatsService::new(url, network, provider, pool).await?;
1241
1242        // If engine events are provided, spawn listener for new payload reporting
1243        let ethstats_for_events = ethstats.clone();
1244        let task_executor = self.task_executor().clone();
1245        task_executor.spawn_task(async move {
1246            while let Some(event) = engine_events.next().await {
1247                use reth_engine_primitives::ConsensusEngineEvent;
1248                match event {
1249                    ConsensusEngineEvent::ForkBlockAdded(executed, duration) |
1250                    ConsensusEngineEvent::CanonicalBlockAdded(executed, duration) => {
1251                        let block_hash = executed.recovered_block.num_hash().hash;
1252                        let block_number = executed.recovered_block.num_hash().number;
1253                        if let Err(e) = ethstats_for_events
1254                            .report_new_payload(block_hash, block_number, duration)
1255                            .await
1256                        {
1257                            debug!(
1258                                target: "ethstats",
1259                                "Failed to report new payload: {}", e
1260                            );
1261                        }
1262                    }
1263                    _ => {
1264                        // Ignore other event types for ethstats reporting
1265                    }
1266                }
1267            }
1268        });
1269
1270        // Spawn main ethstats service
1271        task_executor.spawn_task(async move { ethstats.run().await });
1272
1273        Ok(())
1274    }
1275}
1276
1277/// Joins two attachments together, preserving access to both values.
1278///
1279/// This type enables the launch process to accumulate state while maintaining
1280/// access to all previously attached components. The `left` field holds the
1281/// previous state, while `right` holds the newly attached component.
1282#[derive(Clone, Copy, Debug)]
1283pub struct Attached<L, R> {
1284    left: L,
1285    right: R,
1286}
1287
1288impl<L, R> Attached<L, R> {
1289    /// Creates a new `Attached` with the given values.
1290    pub const fn new(left: L, right: R) -> Self {
1291        Self { left, right }
1292    }
1293
1294    /// Maps the left value to a new value.
1295    pub fn map_left<F, T>(self, f: F) -> Attached<T, R>
1296    where
1297        F: FnOnce(L) -> T,
1298    {
1299        Attached::new(f(self.left), self.right)
1300    }
1301
1302    /// Maps the right value to a new value.
1303    pub fn map_right<F, T>(self, f: F) -> Attached<L, T>
1304    where
1305        F: FnOnce(R) -> T,
1306    {
1307        Attached::new(self.left, f(self.right))
1308    }
1309
1310    /// Get a reference to the left value.
1311    pub const fn left(&self) -> &L {
1312        &self.left
1313    }
1314
1315    /// Get a reference to the right value.
1316    pub const fn right(&self) -> &R {
1317        &self.right
1318    }
1319
1320    /// Get a mutable reference to the left value.
1321    pub const fn left_mut(&mut self) -> &mut L {
1322        &mut self.left
1323    }
1324
1325    /// Get a mutable reference to the right value.
1326    pub const fn right_mut(&mut self) -> &mut R {
1327        &mut self.right
1328    }
1329}
1330
1331/// Helper container type to bundle the initial [`NodeConfig`] and the loaded settings from the
1332/// reth.toml config
1333#[derive(Debug)]
1334pub struct WithConfigs<ChainSpec> {
1335    /// The configured, usually derived from the CLI.
1336    pub config: NodeConfig<ChainSpec>,
1337    /// The loaded reth.toml config.
1338    pub toml_config: reth_config::Config,
1339}
1340
1341impl<ChainSpec> Clone for WithConfigs<ChainSpec> {
1342    fn clone(&self) -> Self {
1343        Self { config: self.config.clone(), toml_config: self.toml_config.clone() }
1344    }
1345}
1346
1347/// Helper container type to bundle the [`ProviderFactory`] and the metrics
1348/// sender.
1349#[derive(Debug, Clone)]
1350pub struct WithMeteredProvider<N: NodeTypesWithDB> {
1351    provider_factory: ProviderFactory<N>,
1352    metrics_sender: UnboundedSender<MetricEvent>,
1353}
1354
1355/// Helper container to bundle the [`ProviderFactory`], [`FullNodeTypes::Provider`]
1356/// and a metrics sender.
1357#[expect(missing_debug_implementations)]
1358pub struct WithMeteredProviders<T>
1359where
1360    T: FullNodeTypes,
1361{
1362    db_provider_container: WithMeteredProvider<NodeTypesWithDBAdapter<T::Types, T::DB>>,
1363    blockchain_db: T::Provider,
1364}
1365
1366/// Helper container to bundle the metered providers container and [`NodeAdapter`].
1367#[expect(missing_debug_implementations)]
1368pub struct WithComponents<T, CB>
1369where
1370    T: FullNodeTypes,
1371    CB: NodeComponentsBuilder<T>,
1372{
1373    db_provider_container: WithMeteredProvider<NodeTypesWithDBAdapter<T::Types, T::DB>>,
1374    node_adapter: NodeAdapter<T, CB::Components>,
1375    head: Head,
1376}
1377
1378/// Returns the metrics hooks for the node.
1379pub fn metrics_hooks<N: NodeTypesWithDB>(provider_factory: &ProviderFactory<N>) -> Hooks {
1380    Hooks::builder()
1381        .with_hook({
1382            let db = provider_factory.db_ref().clone();
1383            move || throttle!(Duration::from_secs(5 * 60), || db.report_metrics())
1384        })
1385        .with_hook({
1386            let sfp = provider_factory.static_file_provider();
1387            move || {
1388                throttle!(Duration::from_secs(5 * 60), || {
1389                    if let Err(error) = sfp.report_metrics() {
1390                        error!(%error, "Failed to report metrics from static file provider");
1391                    }
1392                })
1393            }
1394        })
1395        .with_hook({
1396            let rocksdb = provider_factory.rocksdb_provider();
1397            move || throttle!(Duration::from_secs(5 * 60), || rocksdb.report_metrics())
1398        })
1399        .build()
1400}
1401
1402fn get_partial_trie_unwind_marker(
1403    provider: &(impl MetadataProvider + StageCheckpointReader),
1404) -> ProviderResult<(Option<PartialStateTrieUnwindMarker>, bool)> {
1405    if let Some(marker) = provider.get_metadata(PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY)? {
1406        let marker = serde_json::from_slice::<PartialStateTrieUnwindMarker>(&marker)
1407            .map_err(ProviderError::other)?;
1408        if marker.partial_state_trie >= marker.finish_block_number {
1409            return Err(ProviderError::other(std::io::Error::other(format!(
1410                "partial state trie unwind target #{} is not below original Finish #{}",
1411                marker.partial_state_trie, marker.finish_block_number,
1412            ))))
1413        }
1414        return Ok((Some(marker), true))
1415    }
1416
1417    let Some(finish_checkpoint) = provider.get_stage_checkpoint(StageId::Finish)? else {
1418        return Ok((None, false))
1419    };
1420    let Some(partial_state_trie) =
1421        finish_checkpoint.finish_stage_checkpoint().and_then(|finish| finish.partial_state_trie())
1422    else {
1423        return Ok((None, false))
1424    };
1425
1426    if partial_state_trie > finish_checkpoint.block_number {
1427        return Err(ProviderError::other(std::io::Error::other(format!(
1428            "partial state trie frontier #{partial_state_trie} is ahead of Finish #{}",
1429            finish_checkpoint.block_number,
1430        ))))
1431    }
1432
1433    Ok((
1434        (partial_state_trie < finish_checkpoint.block_number).then_some(
1435            PartialStateTrieUnwindMarker {
1436                finish_block_number: finish_checkpoint.block_number,
1437                partial_state_trie,
1438            },
1439        ),
1440        false,
1441    ))
1442}
1443
1444/// Metadata key for a partial state trie unwind that has not completed yet.
1445const PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY: &str = "partial_state_trie_unwind";
1446
1447fn write_partial_trie_unwind_marker(
1448    provider: &impl MetadataWriter,
1449    marker: PartialStateTrieUnwindMarker,
1450) -> ProviderResult<()> {
1451    provider.write_metadata(
1452        PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY,
1453        serde_json::to_vec(&marker).map_err(ProviderError::other)?,
1454    )
1455}
1456
1457fn delete_partial_trie_unwind_marker(provider: &impl MetadataWriter) -> ProviderResult<()> {
1458    provider.delete_metadata(PARTIAL_STATE_TRIE_UNWIND_METADATA_KEY)
1459}
1460
1461#[cfg(test)]
1462mod tests {
1463    use super::{get_partial_trie_unwind_marker, LaunchContext, NodeConfig};
1464    use reth_config::Config;
1465    use reth_db_api::models::PartialStateTrieUnwindMarker;
1466    use reth_node_core::args::PruningArgs;
1467    use reth_provider::{MetadataProvider, ProviderResult, StageCheckpointReader};
1468    use reth_stages::{FinishCheckpoint, StageCheckpoint, StageId};
1469
1470    const EXTENSION: &str = "toml";
1471
1472    struct MockProvider(Option<Vec<u8>>, Option<StageCheckpoint>);
1473
1474    impl MetadataProvider for MockProvider {
1475        fn get_metadata(&self, _: &str) -> ProviderResult<Option<Vec<u8>>> {
1476            Ok(self.0.clone())
1477        }
1478    }
1479
1480    impl StageCheckpointReader for MockProvider {
1481        fn get_stage_checkpoint(&self, id: StageId) -> ProviderResult<Option<StageCheckpoint>> {
1482            assert_eq!(id, StageId::Finish);
1483            Ok(self.1)
1484        }
1485
1486        fn get_stage_checkpoint_progress(&self, _: StageId) -> ProviderResult<Option<Vec<u8>>> {
1487            Ok(None)
1488        }
1489
1490        fn get_all_checkpoints(&self) -> ProviderResult<Vec<(String, StageCheckpoint)>> {
1491            Ok(Vec::new())
1492        }
1493    }
1494
1495    fn with_tempdir(filename: &str, proc: fn(&std::path::Path)) {
1496        let temp_dir = tempfile::tempdir().unwrap();
1497        let config_path = temp_dir.path().join(filename).with_extension(EXTENSION);
1498        proc(&config_path);
1499        temp_dir.close().unwrap()
1500    }
1501
1502    #[test]
1503    fn test_save_prune_config() {
1504        with_tempdir("prune-store-test", |config_path| {
1505            let mut reth_config = Config::default();
1506            let node_config = NodeConfig {
1507                pruning: PruningArgs {
1508                    full: true,
1509                    minimal: false,
1510                    block_interval: None,
1511                    sender_recovery_full: false,
1512                    sender_recovery_distance: None,
1513                    sender_recovery_before: None,
1514                    transaction_lookup_full: false,
1515                    transaction_lookup_distance: None,
1516                    transaction_lookup_before: None,
1517                    receipts_full: false,
1518                    receipts_pre_merge: false,
1519                    receipts_distance: None,
1520                    receipts_before: None,
1521                    account_history_full: false,
1522                    account_history_distance: None,
1523                    account_history_before: None,
1524                    storage_history_full: false,
1525                    storage_history_distance: None,
1526                    storage_history_before: None,
1527                    bodies_pre_merge: false,
1528                    bodies_distance: None,
1529                    receipts_log_filter: None,
1530                    bodies_before: None,
1531                    minimum_distance: None,
1532                },
1533                ..NodeConfig::test()
1534            };
1535            LaunchContext::save_pruning_config(&mut reth_config, &node_config, config_path)
1536                .unwrap();
1537
1538            let loaded_config = Config::from_path(config_path).unwrap();
1539
1540            assert_eq!(reth_config, loaded_config);
1541        })
1542    }
1543
1544    #[test]
1545    fn get_partial_trie_unwind_marker_uses_partial_finish_checkpoint() {
1546        let finish_checkpoint = StageCheckpoint::new(42)
1547            .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(21) });
1548        let expected =
1549            finish_checkpoint.finish_stage_checkpoint().unwrap().partial_state_trie().map(
1550                |partial_state_trie| PartialStateTrieUnwindMarker {
1551                    finish_block_number: finish_checkpoint.block_number,
1552                    partial_state_trie,
1553                },
1554            );
1555
1556        assert_eq!(
1557            get_partial_trie_unwind_marker(&MockProvider(None, Some(finish_checkpoint))).unwrap(),
1558            (expected, false)
1559        );
1560
1561        let genesis_checkpoint = StageCheckpoint::new(42)
1562            .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(0) });
1563        let expected =
1564            genesis_checkpoint.finish_stage_checkpoint().unwrap().partial_state_trie().map(
1565                |partial_state_trie| PartialStateTrieUnwindMarker {
1566                    finish_block_number: genesis_checkpoint.block_number,
1567                    partial_state_trie,
1568                },
1569            );
1570
1571        assert_eq!(
1572            get_partial_trie_unwind_marker(&MockProvider(None, Some(genesis_checkpoint))).unwrap(),
1573            (expected, false)
1574        );
1575    }
1576
1577    #[test]
1578    fn get_partial_trie_unwind_marker_resumes_persisted_unwind() {
1579        let marker =
1580            PartialStateTrieUnwindMarker { finish_block_number: 42, partial_state_trie: 21 };
1581
1582        assert_eq!(
1583            get_partial_trie_unwind_marker(&MockProvider(
1584                Some(serde_json::to_vec(&marker).unwrap()),
1585                Some(StageCheckpoint::new(21)),
1586            ),)
1587            .unwrap(),
1588            (Some(marker), true)
1589        );
1590        assert_eq!(
1591            get_partial_trie_unwind_marker(&MockProvider(
1592                Some(serde_json::to_vec(&marker).unwrap()),
1593                None
1594            ),)
1595            .unwrap(),
1596            (Some(marker), true)
1597        );
1598    }
1599
1600    #[test]
1601    fn get_partial_trie_unwind_marker_ignores_non_lagging_or_missing_partial_checkpoint() {
1602        let matching_finish_checkpoint = StageCheckpoint::new(42)
1603            .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(42) });
1604        let ahead_finish_checkpoint = StageCheckpoint::new(42)
1605            .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: Some(43) });
1606        let missing_partial_finish_checkpoint = StageCheckpoint::new(42)
1607            .with_finish_stage_checkpoint(FinishCheckpoint { partial_state_trie: None });
1608
1609        assert_eq!(
1610            get_partial_trie_unwind_marker(&MockProvider(None, Some(matching_finish_checkpoint)),)
1611                .unwrap(),
1612            (None, false)
1613        );
1614        assert_eq!(
1615            get_partial_trie_unwind_marker(&MockProvider(
1616                None,
1617                Some(missing_partial_finish_checkpoint)
1618            ),)
1619            .unwrap(),
1620            (None, false)
1621        );
1622        assert_eq!(
1623            get_partial_trie_unwind_marker(&MockProvider(None, None)).unwrap(),
1624            (None, false)
1625        );
1626
1627        let partial_frontier = ahead_finish_checkpoint
1628            .finish_stage_checkpoint()
1629            .and_then(|finish| finish.partial_state_trie());
1630        let result =
1631            get_partial_trie_unwind_marker(&MockProvider(None, Some(ahead_finish_checkpoint)));
1632        if partial_frontier.is_some() {
1633            let error = result.unwrap_err();
1634            assert!(error.to_string().contains("ahead of Finish"), "unexpected error: {error}");
1635        } else {
1636            assert_eq!(result.unwrap(), (None, false));
1637        }
1638    }
1639
1640    #[test]
1641    fn get_partial_trie_unwind_marker_rejects_invalid_persisted_marker() {
1642        let marker =
1643            PartialStateTrieUnwindMarker { finish_block_number: 42, partial_state_trie: 42 };
1644        let error = get_partial_trie_unwind_marker(&MockProvider(
1645            Some(serde_json::to_vec(&marker).unwrap()),
1646            None,
1647        ))
1648        .unwrap_err();
1649
1650        assert!(error.to_string().contains("is not below original Finish"));
1651    }
1652
1653    #[test]
1654    fn get_partial_trie_unwind_marker_rejects_malformed_metadata() {
1655        assert!(get_partial_trie_unwind_marker(&MockProvider(Some(vec![0xff]), None)).is_err());
1656    }
1657}