Skip to main content

reth_cli_commands/stage/
run.rs

1//! Main `stage` command
2//!
3//! Stage debugging tool
4
5use crate::common::{AccessRights, CliNodeComponents, CliNodeTypes, Environment, EnvironmentArgs};
6use alloy_eips::BlockHashOrNumber;
7use backon::{ConstantBuilder, Retryable};
8use clap::Parser;
9use reth_chainspec::{EthChainSpec, EthereumHardforks, Hardforks};
10use reth_cli::chainspec::ChainSpecParser;
11use reth_cli_runner::CliContext;
12use reth_cli_util::get_secret_key;
13use reth_config::config::{HashingConfig, SenderRecoveryConfig, TransactionLookupConfig};
14use reth_downloaders::{
15    bodies::bodies::BodiesDownloaderBuilder,
16    headers::reverse_headers::ReverseHeadersDownloaderBuilder,
17};
18use reth_exex::ExExManagerHandle;
19use reth_network::BlockDownloaderProvider;
20use reth_node_builder::common::metrics_hooks;
21use reth_node_core::{
22    args::{NetworkArgs, StageEnum},
23    utils::get_single_header,
24    version::version_metadata,
25};
26use reth_node_metrics::{
27    chain::ChainSpecInfo,
28    server::{MetricServer, MetricServerConfig},
29    version::VersionInfo,
30};
31use reth_primitives_traits::FastInstant as Instant;
32use reth_provider::{
33    providers::BlockchainProvider, ChainSpecProvider, DBProvider, DatabaseProviderFactory,
34    StageCheckpointReader, StageCheckpointWriter,
35};
36use reth_stages::{
37    stages::{
38        AccountHashingStage, BodyStage, ExecutionStage, HeaderStage, IndexAccountHistoryStage,
39        IndexStorageHistoryStage, MerkleStage, SenderRecoveryStage, StorageHashingStage,
40        TransactionLookupStage,
41    },
42    ExecInput, ExecOutput, ExecutionStageThresholds, Stage, StageExt, UnwindInput, UnwindOutput,
43};
44use std::{net::SocketAddr, sync::Arc};
45use tokio::sync::watch;
46use tracing::*;
47
48/// Number of times fetching the tip header is retried before giving up.
49const HEADER_FETCH_RETRIES: usize = 5;
50
51const HASHING_UNSUPPORTED: &str = "The `hashing` stage cannot be run directly, run `account-hashing` and `storage-hashing` instead.";
52
53/// `reth stage` command
54#[derive(Debug, Parser)]
55pub struct Command<C: ChainSpecParser> {
56    #[command(flatten)]
57    env: EnvironmentArgs<C>,
58
59    /// Enable Prometheus metrics.
60    ///
61    /// The metrics will be served at the given interface and port.
62    #[arg(long, value_name = "SOCKET")]
63    metrics: Option<SocketAddr>,
64
65    /// The name of the stage to run
66    #[arg(value_enum)]
67    stage: StageEnum,
68
69    /// The height to start at
70    #[arg(long)]
71    from: u64,
72
73    /// The end of the stage
74    #[arg(long, short)]
75    to: u64,
76
77    /// Batch size for stage execution and unwind
78    #[arg(long, value_parser = clap::value_parser!(u64).range(1..))]
79    batch_size: Option<u64>,
80
81    /// Normally, running the stage requires unwinding for stages that already
82    /// have been run, in order to not rewrite to the same database slots.
83    ///
84    /// You can optionally skip the unwinding phase if you're syncing a block
85    /// range that has not been synced before.
86    #[arg(long, short)]
87    skip_unwind: bool,
88
89    /// Commits the changes in the database. WARNING: potentially destructive.
90    ///
91    /// Useful when you want to run diagnostics on the database.
92    ///
93    /// NOTE: This flag is currently required for the headers, bodies, and execution stages because
94    /// they use static files and must commit to properly unwind and run.
95    // TODO: We should consider allowing to run hooks at the end of the stage run,
96    // e.g. query the DB size, or any table data.
97    #[arg(long, short)]
98    commit: bool,
99
100    /// Save stage checkpoints
101    #[arg(long)]
102    checkpoints: bool,
103
104    #[command(flatten)]
105    network: NetworkArgs,
106}
107
108impl<C: ChainSpecParser<ChainSpec: EthChainSpec + Hardforks + EthereumHardforks>> Command<C> {
109    /// Execute `stage` command
110    pub async fn execute<N, Comp, F>(self, ctx: CliContext, components: F) -> eyre::Result<()>
111    where
112        N: CliNodeTypes<ChainSpec = C::ChainSpec>,
113        Comp: CliNodeComponents<N>,
114        F: FnOnce(Arc<C::ChainSpec>) -> Comp,
115    {
116        self.validate()?;
117
118        // Quit early if the stage requires a commit and `--commit` is not provided.
119        if self.requires_commit() && !self.commit {
120            return Err(eyre::eyre!(
121                "The stage {} requires overwriting existing static files and must commit, but `--commit` was not provided. Please pass `--commit` and try again.",
122                self.stage.to_string()
123            ));
124        }
125
126        // Raise the fd limit of the process.
127        // Does not do anything on windows.
128        let _ = fdlimit::raise_fd_limit();
129
130        let runtime = ctx.task_executor.clone();
131        let Environment { provider_factory, config, data_dir } =
132            self.env.init::<N>(AccessRights::RW, ctx.task_executor.clone())?;
133
134        let mut provider_rw = provider_factory.database_provider_rw()?;
135        let components = components(provider_factory.chain_spec());
136
137        if let Some(listen_addr) = self.metrics {
138            let config = MetricServerConfig::new(
139                listen_addr,
140                VersionInfo {
141                    version: version_metadata().cargo_pkg_version.as_ref(),
142                    build_timestamp: version_metadata().vergen_build_timestamp.as_ref(),
143                    cargo_features: version_metadata().vergen_cargo_features.as_ref(),
144                    git_sha: version_metadata().vergen_git_sha.as_ref(),
145                    target_triple: version_metadata().vergen_cargo_target_triple.as_ref(),
146                    build_profile: version_metadata().build_profile_name.as_ref(),
147                },
148                ChainSpecInfo { name: provider_factory.chain_spec().chain().to_string() },
149                ctx.task_executor,
150                metrics_hooks(&provider_factory),
151                data_dir.pprof_dumps(),
152            );
153
154            MetricServer::new(config).serve().await?;
155        }
156
157        let batch_size = self.batch_size.unwrap_or(self.to - self.from + 1);
158
159        let etl_config = config.stages.etl.clone();
160        let prune_modes = config.prune.segments.clone();
161
162        let (mut exec_stage, mut unwind_stage): (Box<dyn Stage<_>>, Option<Box<dyn Stage<_>>>) =
163            match self.stage {
164                StageEnum::Headers => {
165                    let consensus = Arc::new(components.consensus().clone());
166
167                    let network_secret_path = self
168                        .network
169                        .p2p_secret_key
170                        .clone()
171                        .unwrap_or_else(|| data_dir.p2p_secret());
172                    let p2p_secret_key = get_secret_key(&network_secret_path)?;
173
174                    let default_peers_path = data_dir.known_peers();
175
176                    let network = self
177                        .network
178                        .network_config::<N::NetworkPrimitives>(
179                            &config,
180                            provider_factory.chain_spec(),
181                            p2p_secret_key,
182                            default_peers_path,
183                            runtime.clone(),
184                        )
185                        .build(BlockchainProvider::new(provider_factory.clone())?)
186                        .start_network()
187                        .await?;
188                    let fetch_client = Arc::new(network.fetch_client().await?);
189
190                    // Use `to` as the tip for the stage
191                    let tip = (|| {
192                        get_single_header(fetch_client.clone(), BlockHashOrNumber::Number(self.to))
193                    })
194                    .retry(ConstantBuilder::default().with_max_times(HEADER_FETCH_RETRIES))
195                    .notify(|err, _| {
196                        warn!(target: "reth::cli", error = %err, "Error requesting header. Retrying...")
197                    })
198                    .await?;
199                    let (_, rx) = watch::channel(tip.hash());
200                    (
201                        Box::new(HeaderStage::new(
202                            provider_factory.clone(),
203                            ReverseHeadersDownloaderBuilder::new(config.stages.headers)
204                                .build(fetch_client, consensus.clone()),
205                            rx,
206                            etl_config,
207                        )),
208                        None,
209                    )
210                }
211                StageEnum::Bodies => {
212                    let consensus = Arc::new(components.consensus().clone());
213
214                    let mut config = config;
215                    config.peers.trusted_nodes_only |= self.network.trusted_only;
216                    config.peers.trusted_nodes.extend(self.network.trusted_peers.clone());
217
218                    let network_secret_path = self
219                        .network
220                        .p2p_secret_key
221                        .clone()
222                        .unwrap_or_else(|| data_dir.p2p_secret());
223                    let p2p_secret_key = get_secret_key(&network_secret_path)?;
224
225                    let default_peers_path = data_dir.known_peers();
226
227                    let network = self
228                        .network
229                        .network_config::<N::NetworkPrimitives>(
230                            &config,
231                            provider_factory.chain_spec(),
232                            p2p_secret_key,
233                            default_peers_path,
234                            runtime.clone(),
235                        )
236                        .build(BlockchainProvider::new(provider_factory.clone())?)
237                        .start_network()
238                        .await?;
239                    let fetch_client = Arc::new(network.fetch_client().await?);
240
241                    let stage = BodyStage::new(
242                        BodiesDownloaderBuilder::default()
243                            .with_stream_batch_size(batch_size as usize)
244                            .with_request_limit(config.stages.bodies.downloader_request_limit)
245                            .with_max_buffered_blocks_size_bytes(
246                                config.stages.bodies.downloader_max_buffered_blocks_size_bytes,
247                            )
248                            .with_concurrent_requests_range(
249                                config.stages.bodies.downloader_min_concurrent_requests..=
250                                    config.stages.bodies.downloader_max_concurrent_requests,
251                            )
252                            .build(fetch_client, consensus.clone(), provider_factory.clone()),
253                    );
254                    (Box::new(stage), None)
255                }
256                StageEnum::Senders => (
257                    Box::new(SenderRecoveryStage::new(
258                        SenderRecoveryConfig { commit_threshold: batch_size },
259                        None,
260                    )),
261                    None,
262                ),
263                StageEnum::Execution => (
264                    Box::new(ExecutionStage::new(
265                        components.evm_config().clone(),
266                        Arc::new(components.consensus().clone()),
267                        ExecutionStageThresholds {
268                            max_blocks: Some(batch_size),
269                            max_changes: None,
270                            max_cumulative_gas: None,
271                            max_duration: None,
272                        },
273                        config.stages.merkle.incremental_threshold,
274                        ExExManagerHandle::empty(),
275                    )),
276                    None,
277                ),
278                StageEnum::TxLookup => (
279                    Box::new(TransactionLookupStage::new(
280                        TransactionLookupConfig { chunk_size: batch_size },
281                        etl_config,
282                        prune_modes.transaction_lookup,
283                    )),
284                    None,
285                ),
286                StageEnum::AccountHashing => (
287                    Box::new(AccountHashingStage::new(
288                        HashingConfig {
289                            clean_threshold: 1,
290                            commit_threshold: batch_size,
291                            commit_entries: u64::MAX,
292                        },
293                        etl_config,
294                    )),
295                    None,
296                ),
297                StageEnum::StorageHashing => (
298                    Box::new(StorageHashingStage::new(
299                        HashingConfig {
300                            clean_threshold: 1,
301                            commit_threshold: batch_size,
302                            commit_entries: u64::MAX,
303                        },
304                        etl_config,
305                    )),
306                    None,
307                ),
308                StageEnum::Merkle => (
309                    Box::new(MerkleStage::new_execution(
310                        config.stages.merkle.rebuild_threshold,
311                        config.stages.merkle.incremental_threshold,
312                    )),
313                    Some(Box::new(MerkleStage::default_unwind())),
314                ),
315                StageEnum::AccountHistory => (
316                    Box::new(IndexAccountHistoryStage::new(
317                        config.stages.index_account_history,
318                        etl_config,
319                        prune_modes.account_history,
320                    )),
321                    None,
322                ),
323                StageEnum::StorageHistory => (
324                    Box::new(IndexStorageHistoryStage::new(
325                        config.stages.index_storage_history,
326                        etl_config,
327                        prune_modes.storage_history,
328                    )),
329                    None,
330                ),
331                StageEnum::Hashing => eyre::bail!(HASHING_UNSUPPORTED),
332            };
333        let checkpoint = provider_rw.get_stage_checkpoint(exec_stage.id())?.unwrap_or_default();
334
335        let unwind_stage = unwind_stage.as_mut().unwrap_or(&mut exec_stage);
336
337        let mut unwind = UnwindInput {
338            checkpoint: checkpoint.with_block_number(self.to),
339            unwind_to: self.from,
340            bad_block: None,
341        };
342
343        if !self.skip_unwind {
344            while unwind.checkpoint.block_number > self.from {
345                let UnwindOutput { checkpoint } = unwind_stage.unwind(&provider_rw, unwind)?;
346                unwind.checkpoint = checkpoint;
347
348                if self.checkpoints {
349                    provider_rw.save_stage_checkpoint(unwind_stage.id(), checkpoint)?;
350                }
351
352                if self.commit {
353                    provider_rw.commit()?;
354                    provider_rw = provider_factory.database_provider_rw()?;
355                }
356            }
357        }
358
359        let mut input = ExecInput {
360            target: Some(self.to),
361            checkpoint: Some(checkpoint.with_block_number(self.from)),
362        };
363
364        let start = Instant::now();
365        info!(target: "reth::cli", stage = %self.stage, "Executing stage");
366        loop {
367            exec_stage.execute_ready(input).await?;
368            let ExecOutput { checkpoint, done } = exec_stage.execute(&provider_rw, input)?;
369
370            input.checkpoint = Some(checkpoint);
371
372            if self.checkpoints {
373                provider_rw.save_stage_checkpoint(exec_stage.id(), checkpoint)?;
374            }
375            if self.commit {
376                provider_rw.commit()?;
377                provider_rw = provider_factory.database_provider_rw()?;
378            }
379
380            if done {
381                break
382            }
383        }
384        info!(target: "reth::cli", stage = %self.stage, time = ?start.elapsed(), "Finished stage");
385
386        Ok(())
387    }
388}
389
390impl<C: ChainSpecParser> Command<C> {
391    /// Returns the underlying chain being used to run this command
392    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
393        Some(&self.env.chain)
394    }
395
396    /// Validates the arguments that do not require opening the database.
397    fn validate(&self) -> eyre::Result<()> {
398        if self.stage == StageEnum::Hashing {
399            eyre::bail!(HASHING_UNSUPPORTED);
400        }
401        eyre::ensure!(
402            self.from <= self.to,
403            "`--from` ({}) must not be greater than `--to` ({})",
404            self.from,
405            self.to
406        );
407        Ok(())
408    }
409
410    /// Returns whether or not the configured stage requires committing.
411    ///
412    /// This is the case for stages that mainly modify static files, as there is no way to unwind
413    /// these stages without committing anyways. This is because static files do not have
414    /// transactions and we cannot change the view of headers without writing.
415    pub fn requires_commit(&self) -> bool {
416        matches!(self.stage, StageEnum::Headers | StageEnum::Bodies | StageEnum::Execution)
417    }
418}