Skip to main content

reth_cli_commands/
common.rs

1//! Contains common `reth` arguments
2
3pub use reth_primitives_traits::header::HeaderMut;
4
5use alloy_primitives::B256;
6use clap::Parser;
7use reth_chainspec::EthChainSpec;
8use reth_cli::chainspec::ChainSpecParser;
9use reth_config::{config::EtlConfig, Config};
10use reth_consensus::noop::NoopConsensus;
11use reth_db::{init_db, open_db_read_only, DatabaseEnv};
12use reth_db_common::init::init_genesis_with_settings;
13use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader};
14use reth_eth_wire::NetPrimitivesFor;
15use reth_evm::{noop::NoopEvmConfig, ConfigureEvm};
16use reth_network::NetworkEventListenerProvider;
17use reth_node_api::FullNodeTypesAdapter;
18use reth_node_builder::{
19    Node, NodeComponents, NodeComponentsBuilder, NodeTypes, NodeTypesWithDBAdapter,
20};
21use reth_node_core::{
22    args::{DatabaseArgs, DatadirArgs, StaticFilesArgs, StorageArgs},
23    dirs::{ChainPath, DataDirPath},
24};
25use reth_provider::{
26    providers::{
27        BlockchainProvider, NodeTypesForProvider, RocksDBProvider, StaticFileProvider,
28        StaticFileProviderBuilder,
29    },
30    ProviderFactory, StaticFileProviderFactory, StorageSettings,
31};
32use reth_stages::{sets::DefaultStages, Pipeline, PipelineTarget};
33use reth_static_file::StaticFileProducer;
34use std::{path::PathBuf, sync::Arc};
35use tokio::sync::watch;
36use tracing::{debug, info, warn};
37
38/// Struct to hold config and datadir paths
39#[derive(Debug, Parser)]
40pub struct EnvironmentArgs<C: ChainSpecParser> {
41    /// Parameters for datadir configuration
42    #[command(flatten)]
43    pub datadir: DatadirArgs,
44
45    /// The path to the configuration file to use.
46    #[arg(long, value_name = "FILE")]
47    pub config: Option<PathBuf>,
48
49    /// The chain this node is running.
50    ///
51    /// Possible values are either a built-in chain or the path to a chain specification file.
52    #[arg(
53        long,
54        value_name = "CHAIN_OR_PATH",
55        long_help = C::help_message(),
56        default_value = C::default_value(),
57        value_parser = C::parser(),
58        global = true
59    )]
60    pub chain: Arc<C::ChainSpec>,
61
62    /// All database related arguments
63    #[command(flatten)]
64    pub db: DatabaseArgs,
65
66    /// All static files related arguments
67    #[command(flatten)]
68    pub static_files: StaticFilesArgs,
69
70    /// Storage mode configuration (v2 vs v1/legacy)
71    #[command(flatten)]
72    pub storage: StorageArgs,
73}
74
75impl<C: ChainSpecParser> EnvironmentArgs<C> {
76    /// Returns the storage settings for new database initialization.
77    ///
78    /// Always returns [`StorageSettings::v2()`] — v2 is the default for all new
79    /// databases. Existing databases use the settings persisted in their metadata.
80    pub fn storage_settings(&self) -> StorageSettings {
81        StorageSettings::v2()
82    }
83
84    /// Initializes environment according to [`AccessRights`] and returns an instance of
85    /// [`Environment`].
86    ///
87    /// The provided `runtime` is used for parallel storage I/O.
88    pub fn init<N: CliNodeTypes>(
89        &self,
90        access: AccessRights,
91        runtime: reth_tasks::Runtime,
92    ) -> eyre::Result<Environment<N>>
93    where
94        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
95    {
96        let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
97        let db_path = data_dir.db();
98        let sf_path = data_dir.static_files();
99        let rocksdb_path = data_dir.rocksdb();
100
101        if access.is_read_write() {
102            reth_fs_util::create_dir_all(&db_path)?;
103            reth_fs_util::create_dir_all(&sf_path)?;
104            reth_fs_util::create_dir_all(&rocksdb_path)?;
105        }
106
107        let config_path = self.config.clone().unwrap_or_else(|| data_dir.config());
108
109        let mut config = Config::from_path(config_path)
110            .inspect_err(
111                |err| warn!(target: "reth::cli", %err, "Failed to load config file, using default"),
112            )
113            .unwrap_or_default();
114
115        // Make sure ETL doesn't default to /tmp/, but to whatever datadir is set to
116        if config.stages.etl.dir.is_none() {
117            config.stages.etl.dir = Some(EtlConfig::from_datadir(data_dir.data_dir()));
118        }
119        if config.stages.era.folder.is_none() {
120            config.stages.era = config.stages.era.with_datadir(data_dir.data_dir());
121        }
122
123        info!(target: "reth::cli", ?db_path, ?sf_path, "Opening storage");
124        let genesis_block_number = self.chain.genesis().number.unwrap_or_default();
125        let (db, sfp) = match access {
126            AccessRights::RW => (
127                init_db(db_path, self.db.database_args())?,
128                StaticFileProviderBuilder::read_write(sf_path)
129                    .with_metrics()
130                    .with_genesis_block_number(genesis_block_number)
131                    .build()?,
132            ),
133            AccessRights::RO | AccessRights::RoInconsistent => {
134                (open_db_read_only(&db_path, self.db.database_args())?, {
135                    let provider = StaticFileProviderBuilder::read_only(sf_path)
136                        .with_metrics()
137                        .with_genesis_block_number(genesis_block_number)
138                        .build()?;
139                    provider.watch_directory();
140                    provider
141                })
142            }
143        };
144        let rocksdb_provider = if !access.is_read_write() && !RocksDBProvider::exists(&rocksdb_path)
145        {
146            // RocksDB database doesn't exist yet (e.g. datadir restored from a snapshot
147            // or created before RocksDB storage). Create an empty one so read-only
148            // commands can proceed.
149            debug!(target: "reth::cli", ?rocksdb_path, "RocksDB not found, initializing empty database");
150            reth_fs_util::create_dir_all(&rocksdb_path)?;
151            RocksDBProvider::builder(data_dir.rocksdb())
152                .with_default_tables()
153                .with_database_log_level(self.db.log_level)
154                .build()?
155        } else {
156            RocksDBProvider::builder(data_dir.rocksdb())
157                .with_default_tables()
158                .with_database_log_level(self.db.log_level)
159                .with_read_only(!access.is_read_write())
160                .build()?
161        };
162
163        let provider_factory =
164            self.create_provider_factory(&config, db, sfp, rocksdb_provider, access, runtime)?;
165        if access.is_read_write() {
166            debug!(target: "reth::cli", chain=%self.chain.chain(), genesis=?self.chain.genesis_hash(), "Initializing genesis");
167            init_genesis_with_settings(&provider_factory, self.storage_settings())?;
168        }
169
170        Ok(Environment { config, provider_factory, data_dir })
171    }
172
173    /// Returns a [`ProviderFactory`] after executing consistency checks.
174    ///
175    /// If it's a read-write environment and an issue is found, it will attempt to heal (including a
176    /// pipeline unwind). Otherwise, it will print out a warning, advising the user to restart the
177    /// node to heal.
178    fn create_provider_factory<N: CliNodeTypes>(
179        &self,
180        config: &Config,
181        db: DatabaseEnv,
182        static_file_provider: StaticFileProvider<N::Primitives>,
183        rocksdb_provider: RocksDBProvider,
184        access: AccessRights,
185        runtime: reth_tasks::Runtime,
186    ) -> eyre::Result<ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>>
187    where
188        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
189    {
190        let factory = ProviderFactory::<NodeTypesWithDBAdapter<N, DatabaseEnv>>::new(
191            db,
192            self.chain.clone(),
193            static_file_provider,
194            rocksdb_provider,
195            runtime,
196        )?
197        .with_prune_modes(config.prune.segments.clone())
198        .with_minimum_pruning_distance(config.prune.minimum_pruning_distance);
199
200        // Check for consistency between database and static files.
201        if !access.is_read_only_inconsistent() &&
202            let Some(unwind_target) =
203                factory.static_file_provider().check_consistency(&factory.provider()?)?
204        {
205            if factory.db_ref().is_read_only()? {
206                warn!(target: "reth::cli", ?unwind_target, "Inconsistent storage. Restart node to heal.");
207                return Ok(factory)
208            }
209
210            // Highly unlikely to happen, and given its destructive nature, it's better to panic
211            // instead.
212            assert_ne!(
213                unwind_target,
214                PipelineTarget::Unwind(0),
215                "A static file <> database inconsistency was found that would trigger an unwind to block 0"
216            );
217
218            info!(target: "reth::cli", unwind_target = %unwind_target, "Executing an unwind after a failed storage consistency check.");
219
220            let (_tip_tx, tip_rx) = watch::channel(B256::ZERO);
221
222            // Builds and executes an unwind-only pipeline
223            let mut pipeline = Pipeline::<NodeTypesWithDBAdapter<N, DatabaseEnv>>::builder()
224                .add_stages(DefaultStages::new(
225                    factory.clone(),
226                    tip_rx,
227                    Arc::new(NoopConsensus::default()),
228                    NoopHeaderDownloader::default(),
229                    NoopBodiesDownloader::default(),
230                    NoopEvmConfig::<N::Evm>::default(),
231                    config.stages.clone(),
232                    config.prune.segments.clone(),
233                    None,
234                ))
235                .build(
236                    factory.clone(),
237                    StaticFileProducer::new(factory.clone(), config.prune.segments.clone()),
238                );
239
240            // Move all applicable data from database to static files.
241            pipeline.move_to_static_files()?;
242            pipeline.unwind(unwind_target.unwind_target().expect("should exist"), None)?;
243        }
244
245        Ok(factory)
246    }
247}
248
249/// Environment built from [`EnvironmentArgs`].
250#[derive(Debug)]
251pub struct Environment<N: NodeTypes> {
252    /// Configuration for reth node
253    pub config: Config,
254    /// Provider factory.
255    pub provider_factory: ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
256    /// Datadir path.
257    pub data_dir: ChainPath<DataDirPath>,
258}
259
260/// Environment access rights.
261#[derive(Debug, Copy, Clone)]
262pub enum AccessRights {
263    /// Read-write access
264    RW,
265    /// Read-only access
266    RO,
267    /// Read-only access with possibly inconsistent data
268    RoInconsistent,
269}
270
271impl AccessRights {
272    /// Returns `true` if it requires read-write access to the environment.
273    pub const fn is_read_write(&self) -> bool {
274        matches!(self, Self::RW)
275    }
276
277    /// Returns `true` if it requires read-only access to the environment with possibly inconsistent
278    /// data.
279    pub const fn is_read_only_inconsistent(&self) -> bool {
280        matches!(self, Self::RoInconsistent)
281    }
282}
283
284/// Helper alias to satisfy `FullNodeTypes` bound on [`Node`] trait generic.
285type FullTypesAdapter<T> = FullNodeTypesAdapter<
286    T,
287    DatabaseEnv,
288    BlockchainProvider<NodeTypesWithDBAdapter<T, DatabaseEnv>>,
289>;
290
291/// Helper trait with a common set of requirements for the
292/// [`NodeTypes`] in CLI.
293pub trait CliNodeTypes: Node<FullTypesAdapter<Self>> + NodeTypesForProvider {
294    type Evm: ConfigureEvm<Primitives = Self::Primitives>;
295    type NetworkPrimitives: NetPrimitivesFor<Self::Primitives>;
296}
297
298impl<N> CliNodeTypes for N
299where
300    N: Node<FullTypesAdapter<Self>> + NodeTypesForProvider,
301{
302    type Evm = <<N::ComponentsBuilder as NodeComponentsBuilder<FullTypesAdapter<Self>>>::Components as NodeComponents<FullTypesAdapter<Self>>>::Evm;
303    type NetworkPrimitives = <<<N::ComponentsBuilder as NodeComponentsBuilder<FullTypesAdapter<Self>>>::Components as NodeComponents<FullTypesAdapter<Self>>>::Network as NetworkEventListenerProvider>::Primitives;
304}
305
306type EvmFor<N> = <<<N as Node<FullTypesAdapter<N>>>::ComponentsBuilder as NodeComponentsBuilder<
307    FullTypesAdapter<N>,
308>>::Components as NodeComponents<FullTypesAdapter<N>>>::Evm;
309
310type ConsensusFor<N> =
311    <<<N as Node<FullTypesAdapter<N>>>::ComponentsBuilder as NodeComponentsBuilder<
312        FullTypesAdapter<N>,
313    >>::Components as NodeComponents<FullTypesAdapter<N>>>::Consensus;
314
315/// Helper trait aggregating components required for the CLI.
316pub trait CliNodeComponents<N: CliNodeTypes>: Send + Sync + 'static {
317    /// Returns the configured EVM.
318    fn evm_config(&self) -> &EvmFor<N>;
319    /// Returns the consensus implementation.
320    fn consensus(&self) -> &ConsensusFor<N>;
321}
322
323impl<N: CliNodeTypes> CliNodeComponents<N> for (EvmFor<N>, ConsensusFor<N>) {
324    fn evm_config(&self) -> &EvmFor<N> {
325        &self.0
326    }
327
328    fn consensus(&self) -> &ConsensusFor<N> {
329        &self.1
330    }
331}
332
333/// Helper trait alias for an [`FnOnce`] producing [`CliNodeComponents`].
334pub trait CliComponentsBuilder<N: CliNodeTypes>:
335    FnOnce(Arc<N::ChainSpec>) -> Self::Components + Send + Sync + 'static
336{
337    type Components: CliNodeComponents<N>;
338}
339
340impl<N: CliNodeTypes, F, Comp> CliComponentsBuilder<N> for F
341where
342    F: FnOnce(Arc<N::ChainSpec>) -> Comp + Send + Sync + 'static,
343    Comp: CliNodeComponents<N>,
344{
345    type Components = Comp;
346}