Skip to main content

reth_cli_commands/stage/
unwind.rs

1//! Unwinding a certain block range
2
3use crate::{
4    common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs},
5    stage::CliNodeComponents,
6};
7use alloy_eips::BlockHashOrNumber;
8use alloy_primitives::B256;
9use clap::{Parser, Subcommand};
10use reth_chainspec::{ChainSpecProvider, EthChainSpec, EthereumHardforks};
11use reth_cli::chainspec::ChainSpecParser;
12use reth_config::Config;
13use reth_consensus::noop::NoopConsensus;
14use reth_db::DatabaseEnv;
15use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader};
16use reth_evm::ConfigureEvm;
17use reth_exex::ExExManagerHandle;
18use reth_provider::{providers::ProviderNodeTypes, BlockNumReader, ProviderFactory};
19use reth_stages::{
20    sets::{DefaultStages, OfflineStages},
21    stages::{EraImportSource, EraStage, ExecutionStage},
22    ExecutionStageThresholds, Pipeline, StageId, StageSet,
23};
24use reth_static_file::StaticFileProducer;
25use std::sync::Arc;
26use tokio::sync::watch;
27use tracing::info;
28
29/// `reth stage unwind` command
30#[derive(Debug, Parser)]
31pub struct Command<C: ChainSpecParser> {
32    #[command(flatten)]
33    env: EnvironmentArgs<C>,
34
35    #[command(subcommand)]
36    command: Subcommands,
37
38    /// If this is enabled, then all stages except headers, bodies, and sender recovery will be
39    /// unwound.
40    #[arg(long)]
41    offline: bool,
42}
43
44impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
45    /// Execute `db stage unwind` command
46    pub async fn execute<N: CliNodeTypes<ChainSpec = C::ChainSpec>, F, Comp>(
47        self,
48        components: F,
49        runtime: reth_tasks::Runtime,
50    ) -> eyre::Result<()>
51    where
52        Comp: CliNodeComponents<N>,
53        F: FnOnce(Arc<C::ChainSpec>) -> Comp,
54    {
55        let Environment { provider_factory, config, data_dir: _ } =
56            self.env.init::<N>(AccessRights::RW, runtime)?;
57
58        let target = self.command.unwind_target(provider_factory.clone())?;
59
60        let components = components(provider_factory.chain_spec());
61
62        if self.offline {
63            info!(target: "reth::cli", "Performing an unwind for offline-only data!");
64        }
65
66        let highest_static_file_block = provider_factory.provider()?.last_block_number()?;
67        info!(target: "reth::cli", ?target, ?highest_static_file_block, prune_config=?config.prune,  "Executing a pipeline unwind.");
68
69        // This will build an offline-only pipeline if the `offline` flag is enabled
70        let mut pipeline =
71            self.build_pipeline(config, provider_factory, components.evm_config().clone())?;
72
73        // Move all applicable data from database to static files.
74        pipeline.move_to_static_files()?;
75
76        pipeline.unwind(target, None)?;
77
78        info!(target: "reth::cli", ?target, "Unwound blocks");
79
80        Ok(())
81    }
82
83    fn build_pipeline<N: ProviderNodeTypes<ChainSpec = C::ChainSpec>>(
84        self,
85        config: Config,
86        provider_factory: ProviderFactory<N>,
87        evm_config: impl ConfigureEvm<Primitives = N::Primitives> + 'static,
88    ) -> Result<Pipeline<N>, eyre::Error> {
89        let stage_conf = &config.stages;
90        let prune_modes = config.prune.segments.clone();
91
92        let (tip_tx, tip_rx) = watch::channel(B256::ZERO);
93
94        let builder = if self.offline {
95            Pipeline::<N>::builder().add_stages(
96                OfflineStages::new(
97                    evm_config,
98                    NoopConsensus::arc(),
99                    config.stages,
100                    prune_modes.clone(),
101                )
102                .builder()
103                .disable(reth_stages::StageId::SenderRecovery),
104            )
105        } else {
106            Pipeline::<N>::builder().with_tip_sender(tip_tx).add_stages(
107                DefaultStages::new(
108                    provider_factory.clone(),
109                    tip_rx,
110                    Arc::new(NoopConsensus::default()),
111                    NoopHeaderDownloader::default(),
112                    NoopBodiesDownloader::default(),
113                    evm_config.clone(),
114                    stage_conf.clone(),
115                    prune_modes.clone(),
116                    None,
117                )
118                .builder()
119                .add_before(
120                    EraStage::new(None::<EraImportSource>, stage_conf.etl.clone()),
121                    StageId::Headers,
122                )
123                .set(ExecutionStage::new(
124                    evm_config,
125                    Arc::new(NoopConsensus::default()),
126                    ExecutionStageThresholds {
127                        max_blocks: None,
128                        max_changes: None,
129                        max_cumulative_gas: None,
130                        max_duration: None,
131                    },
132                    stage_conf.execution_external_clean_threshold(),
133                    ExExManagerHandle::empty(),
134                )),
135            )
136        };
137
138        let pipeline = builder.build(
139            provider_factory.clone(),
140            StaticFileProducer::new(provider_factory, prune_modes),
141        );
142        Ok(pipeline)
143    }
144}
145
146impl<C: ChainSpecParser> Command<C> {
147    /// Return the underlying chain being used to run this command
148    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
149        Some(&self.env.chain)
150    }
151}
152
153/// `reth stage unwind` subcommand
154#[derive(Subcommand, Debug, Eq, PartialEq)]
155enum Subcommands {
156    /// Unwinds the database from the latest block, until the given block number or hash has been
157    /// reached, that block is not included.
158    #[command(name = "to-block")]
159    ToBlock { target: BlockHashOrNumber },
160    /// Unwinds the database from the latest block, until the given number of blocks have been
161    /// reached.
162    #[command(name = "num-blocks")]
163    NumBlocks { amount: u64 },
164}
165
166impl Subcommands {
167    /// Returns the block to unwind to. The returned block will stay in database.
168    fn unwind_target<N: ProviderNodeTypes<DB = DatabaseEnv>>(
169        &self,
170        factory: ProviderFactory<N>,
171    ) -> eyre::Result<u64> {
172        let provider = factory.provider()?;
173        let last = provider.last_block_number()?;
174        let target = match self {
175            Self::ToBlock { target } => match target {
176                BlockHashOrNumber::Hash(hash) => provider
177                    .block_number(*hash)?
178                    .ok_or_else(|| eyre::eyre!("Block hash not found in database: {hash:?}"))?,
179                BlockHashOrNumber::Number(num) => *num,
180            },
181            Self::NumBlocks { amount } => last.saturating_sub(*amount),
182        };
183        if target > last {
184            eyre::bail!(
185                "Target block number {target} is higher than the latest block number {last}"
186            );
187        }
188        Ok(target)
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use reth_chainspec::SEPOLIA;
196    use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
197
198    #[test]
199    fn parse_unwind() {
200        let cmd = Command::<EthereumChainSpecParser>::parse_from([
201            "reth",
202            "--datadir",
203            "dir",
204            "to-block",
205            "100",
206        ]);
207        assert_eq!(cmd.command, Subcommands::ToBlock { target: BlockHashOrNumber::Number(100) });
208
209        let cmd = Command::<EthereumChainSpecParser>::parse_from([
210            "reth",
211            "--datadir",
212            "dir",
213            "num-blocks",
214            "100",
215        ]);
216        assert_eq!(cmd.command, Subcommands::NumBlocks { amount: 100 });
217    }
218
219    #[test]
220    fn parse_unwind_chain() {
221        let cmd = Command::<EthereumChainSpecParser>::parse_from([
222            "reth", "--chain", "sepolia", "to-block", "100",
223        ]);
224        assert_eq!(cmd.command, Subcommands::ToBlock { target: BlockHashOrNumber::Number(100) });
225        assert_eq!(cmd.env.chain.chain_id(), SEPOLIA.chain_id());
226    }
227}