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