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    BalStoreHandle, ProviderFactory, RocksDBBalStore, 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    /// Determined by the `--storage.v2` flag (defaults to `true`).
79    /// Existing databases retain whatever settings are persisted in their
80    /// metadata (checked during genesis init).
81    pub fn storage_settings(&self) -> StorageSettings {
82        if self.storage.v2 {
83            StorageSettings::v2()
84        } else {
85            StorageSettings::v1()
86        }
87    }
88
89    /// Initializes environment according to [`AccessRights`] and returns an instance of
90    /// [`Environment`].
91    ///
92    /// The provided `runtime` is used for parallel storage I/O.
93    pub fn init<N: CliNodeTypes>(
94        &self,
95        access: AccessRights,
96        runtime: reth_tasks::Runtime,
97    ) -> eyre::Result<Environment<N>>
98    where
99        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
100    {
101        let data_dir = self.datadir.clone().resolve_datadir(self.chain.chain());
102        let db_path = data_dir.db();
103        let sf_path = data_dir.static_files();
104        let rocksdb_path = data_dir.rocksdb();
105
106        if access.is_read_write() {
107            reth_fs_util::create_dir_all(&db_path)?;
108            reth_fs_util::create_dir_all(&sf_path)?;
109            reth_fs_util::create_dir_all(&rocksdb_path)?;
110        }
111
112        let config_path = self.config.clone().unwrap_or_else(|| data_dir.config());
113
114        let mut config = Config::from_path(config_path)
115            .inspect_err(
116                |err| warn!(target: "reth::cli", %err, "Failed to load config file, using default"),
117            )
118            .unwrap_or_default();
119
120        // Make sure ETL doesn't default to /tmp/, but to whatever datadir is set to
121        if config.stages.etl.dir.is_none() {
122            config.stages.etl.dir = Some(EtlConfig::from_datadir(data_dir.data_dir()));
123        }
124        if config.stages.era.folder.is_none() {
125            config.stages.era = config.stages.era.with_datadir(data_dir.data_dir());
126        }
127
128        info!(target: "reth::cli", ?db_path, ?sf_path, "Opening storage");
129        let genesis_block_number = self.chain.genesis().number.unwrap_or_default();
130        let (db, sfp) = match access {
131            AccessRights::RW | AccessRights::RwInconsistent => (
132                init_db(db_path, self.db.database_args())?,
133                StaticFileProviderBuilder::read_write(sf_path)
134                    .with_metrics()
135                    .with_genesis_block_number(genesis_block_number)
136                    .build()?,
137            ),
138            AccessRights::RO | AccessRights::RoInconsistent => (
139                open_db_read_only(&db_path, self.db.database_args())?,
140                StaticFileProviderBuilder::read_only(sf_path)
141                    .with_metrics()
142                    .with_genesis_block_number(genesis_block_number)
143                    .build()?,
144            ),
145        };
146        let rocksdb_provider = if !access.is_read_write() && !RocksDBProvider::exists(&rocksdb_path)
147        {
148            // RocksDB database doesn't exist yet (e.g. datadir restored from a snapshot
149            // or created before RocksDB storage). Create an empty one so read-only
150            // commands can proceed.
151            debug!(target: "reth::cli", ?rocksdb_path, "RocksDB not found, initializing empty database");
152            reth_fs_util::create_dir_all(&rocksdb_path)?;
153            let mut builder = RocksDBProvider::builder(data_dir.rocksdb())
154                .with_default_tables()
155                .with_database_log_level(self.db.log_level);
156            if let Some(cache_size) = self.db.rocksdb_block_cache_size {
157                builder = builder.with_block_cache_size(cache_size);
158            }
159            builder.build()?
160        } else {
161            let mut builder = RocksDBProvider::builder(data_dir.rocksdb())
162                .with_default_tables()
163                .with_database_log_level(self.db.log_level)
164                .with_read_only(!access.is_read_write());
165            if let Some(cache_size) = self.db.rocksdb_block_cache_size {
166                builder = builder.with_block_cache_size(cache_size);
167            }
168            builder.build()?
169        };
170
171        let provider_factory =
172            self.create_provider_factory(&config, db, sfp, rocksdb_provider, access, runtime)?;
173        if access.is_read_write() {
174            debug!(target: "reth::cli", chain=%self.chain.chain(), genesis=?self.chain.genesis_hash(), "Initializing genesis");
175            init_genesis_with_settings(&provider_factory, self.storage_settings())?;
176        }
177
178        Ok(Environment { config, provider_factory, data_dir })
179    }
180
181    /// Returns a [`ProviderFactory`] after executing consistency checks unless `access` permits
182    /// inconsistent storage.
183    ///
184    /// Checked read-write access heals inconsistencies (including a pipeline unwind), while checked
185    /// read-only access warns that the node must be restarted to heal.
186    fn create_provider_factory<N: CliNodeTypes>(
187        &self,
188        config: &Config,
189        db: DatabaseEnv,
190        static_file_provider: StaticFileProvider<N::Primitives>,
191        rocksdb_provider: RocksDBProvider,
192        access: AccessRights,
193        runtime: reth_tasks::Runtime,
194    ) -> eyre::Result<ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>>
195    where
196        C: ChainSpecParser<ChainSpec = N::ChainSpec>,
197    {
198        let bal_store = self
199            .db
200            .balstore_cache_size
201            .map(|distance| {
202                RocksDBBalStore::with_buffer_retention_distance(rocksdb_provider.clone(), distance)
203            })
204            .unwrap_or_else(|| RocksDBBalStore::new(rocksdb_provider.clone()));
205        let bal_store = BalStoreHandle::new(bal_store);
206        let factory = ProviderFactory::<NodeTypesWithDBAdapter<N, DatabaseEnv>>::new(
207            db,
208            self.chain.clone(),
209            static_file_provider,
210            rocksdb_provider,
211            runtime,
212        )?
213        .with_prune_modes(config.prune.segments.clone())
214        .with_minimum_pruning_distance(config.prune.minimum_pruning_distance)
215        .with_bal_store(bal_store);
216
217        // Check for consistency between database and static files.
218        if !access.skips_consistency_check() &&
219            let Some(unwind_target) =
220                factory.static_file_provider().check_consistency(&factory.provider()?)?
221        {
222            if factory.db_ref().is_read_only()? {
223                warn!(target: "reth::cli", ?unwind_target, "Inconsistent storage. Restart node to heal.");
224                return Ok(factory)
225            }
226
227            // Highly unlikely to happen, and given its destructive nature, it's better to panic
228            // instead.
229            assert_ne!(
230                unwind_target,
231                PipelineTarget::Unwind(0),
232                "A static file <> database inconsistency was found that would trigger an unwind to block 0"
233            );
234
235            info!(target: "reth::cli", unwind_target = %unwind_target, "Executing an unwind after a failed storage consistency check.");
236
237            let (_tip_tx, tip_rx) = watch::channel(B256::ZERO);
238
239            // Builds and executes an unwind-only pipeline
240            let mut pipeline = Pipeline::<NodeTypesWithDBAdapter<N, DatabaseEnv>>::builder()
241                .add_stages(DefaultStages::new(
242                    factory.clone(),
243                    tip_rx,
244                    Arc::new(NoopConsensus::default()),
245                    NoopHeaderDownloader::default(),
246                    NoopBodiesDownloader::default(),
247                    NoopEvmConfig::<N::Evm>::default(),
248                    config.stages.clone(),
249                    config.prune.segments.clone(),
250                    None,
251                ))
252                .build(
253                    factory.clone(),
254                    StaticFileProducer::new(factory.clone(), config.prune.segments.clone()),
255                );
256
257            // Move all applicable data from database to static files.
258            pipeline.move_to_static_files()?;
259            pipeline.unwind(unwind_target.unwind_target().expect("should exist"), None)?;
260        }
261
262        Ok(factory)
263    }
264}
265
266/// Environment built from [`EnvironmentArgs`].
267#[derive(Debug)]
268pub struct Environment<N: NodeTypes> {
269    /// Configuration for reth node
270    pub config: Config,
271    /// Provider factory.
272    pub provider_factory: ProviderFactory<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
273    /// Datadir path.
274    pub data_dir: ChainPath<DataDirPath>,
275}
276
277/// Environment access rights.
278#[derive(Debug, Copy, Clone)]
279pub enum AccessRights {
280    /// Read-write access
281    RW,
282    /// Read-write access with possibly inconsistent data
283    RwInconsistent,
284    /// Read-only access
285    RO,
286    /// Read-only access with possibly inconsistent data
287    RoInconsistent,
288}
289
290impl AccessRights {
291    /// Returns `true` if it requires read-write access to the environment.
292    pub const fn is_read_write(&self) -> bool {
293        matches!(self, Self::RW | Self::RwInconsistent)
294    }
295
296    /// Returns `true` if it requires read-only access to the environment with possibly inconsistent
297    /// data.
298    pub const fn is_read_only_inconsistent(&self) -> bool {
299        matches!(self, Self::RoInconsistent)
300    }
301
302    /// Returns `true` if storage consistency checks should be skipped.
303    pub const fn skips_consistency_check(&self) -> bool {
304        matches!(self, Self::RwInconsistent | Self::RoInconsistent)
305    }
306}
307
308/// Helper alias to satisfy `FullNodeTypes` bound on [`Node`] trait generic.
309type FullTypesAdapter<T> = FullNodeTypesAdapter<
310    T,
311    DatabaseEnv,
312    BlockchainProvider<NodeTypesWithDBAdapter<T, DatabaseEnv>>,
313>;
314
315/// Helper trait with a common set of requirements for the
316/// [`NodeTypes`] in CLI.
317pub trait CliNodeTypes: Node<FullTypesAdapter<Self>> + NodeTypesForProvider {
318    type Evm: ConfigureEvm<Primitives = Self::Primitives>;
319    type NetworkPrimitives: NetPrimitivesFor<Self::Primitives>;
320}
321
322impl<N> CliNodeTypes for N
323where
324    N: Node<FullTypesAdapter<Self>> + NodeTypesForProvider,
325{
326    type Evm = <<N::ComponentsBuilder as NodeComponentsBuilder<FullTypesAdapter<Self>>>::Components as NodeComponents<FullTypesAdapter<Self>>>::Evm;
327    type NetworkPrimitives = <<<N::ComponentsBuilder as NodeComponentsBuilder<FullTypesAdapter<Self>>>::Components as NodeComponents<FullTypesAdapter<Self>>>::Network as NetworkEventListenerProvider>::Primitives;
328}
329
330type EvmFor<N> = <<<N as Node<FullTypesAdapter<N>>>::ComponentsBuilder as NodeComponentsBuilder<
331    FullTypesAdapter<N>,
332>>::Components as NodeComponents<FullTypesAdapter<N>>>::Evm;
333
334type ConsensusFor<N> =
335    <<<N as Node<FullTypesAdapter<N>>>::ComponentsBuilder as NodeComponentsBuilder<
336        FullTypesAdapter<N>,
337    >>::Components as NodeComponents<FullTypesAdapter<N>>>::Consensus;
338
339/// Helper trait aggregating components required for the CLI.
340pub trait CliNodeComponents<N: CliNodeTypes>: Send + Sync + 'static {
341    /// Returns the configured EVM.
342    fn evm_config(&self) -> &EvmFor<N>;
343    /// Returns the consensus implementation.
344    fn consensus(&self) -> &ConsensusFor<N>;
345}
346
347impl<N: CliNodeTypes> CliNodeComponents<N> for (EvmFor<N>, ConsensusFor<N>) {
348    fn evm_config(&self) -> &EvmFor<N> {
349        &self.0
350    }
351
352    fn consensus(&self) -> &ConsensusFor<N> {
353        &self.1
354    }
355}
356
357/// Helper trait alias for an [`FnOnce`] producing [`CliNodeComponents`].
358pub trait CliComponentsBuilder<N: CliNodeTypes>:
359    FnOnce(Arc<N::ChainSpec>) -> Self::Components + Send + Sync + 'static
360{
361    type Components: CliNodeComponents<N>;
362}
363
364impl<N: CliNodeTypes, F, Comp> CliComponentsBuilder<N> for F
365where
366    F: FnOnce(Arc<N::ChainSpec>) -> Comp + Send + Sync + 'static,
367    Comp: CliNodeComponents<N>,
368{
369    type Components = Comp;
370}
371
372#[cfg(test)]
373mod tests {
374    use super::AccessRights;
375
376    #[test]
377    fn inconsistent_access_rights_skip_consistency_checks() {
378        assert!(AccessRights::RwInconsistent.is_read_write());
379        assert!(AccessRights::RwInconsistent.skips_consistency_check());
380        assert!(!AccessRights::RW.skips_consistency_check());
381
382        assert!(!AccessRights::RoInconsistent.is_read_write());
383        assert!(AccessRights::RoInconsistent.is_read_only_inconsistent());
384        assert!(AccessRights::RoInconsistent.skips_consistency_check());
385        assert!(!AccessRights::RO.skips_consistency_check());
386    }
387}