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::ExecutionStage,
22    ExecutionStageThresholds, Pipeline, 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                .set(ExecutionStage::new(
119                    evm_config,
120                    Arc::new(NoopConsensus::default()),
121                    ExecutionStageThresholds {
122                        max_blocks: None,
123                        max_changes: None,
124                        max_cumulative_gas: None,
125                        max_duration: None,
126                    },
127                    stage_conf.execution_external_clean_threshold(),
128                    ExExManagerHandle::empty(),
129                )),
130            )
131        };
132
133        let pipeline = builder.build(
134            provider_factory.clone(),
135            StaticFileProducer::new(provider_factory, prune_modes),
136        );
137        Ok(pipeline)
138    }
139}
140
141impl<C: ChainSpecParser> Command<C> {
142    /// Return the underlying chain being used to run this command
143    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
144        Some(&self.env.chain)
145    }
146}
147
148/// `reth stage unwind` subcommand
149#[derive(Subcommand, Debug, Eq, PartialEq)]
150enum Subcommands {
151    /// Unwinds the database from the latest block, until the given block number or hash has been
152    /// reached, that block is not included.
153    #[command(name = "to-block")]
154    ToBlock { target: BlockHashOrNumber },
155    /// Unwinds the database from the latest block, until the given number of blocks have been
156    /// reached.
157    #[command(name = "num-blocks")]
158    NumBlocks { amount: u64 },
159}
160
161impl Subcommands {
162    /// Returns the block to unwind to. The returned block will stay in database.
163    fn unwind_target<N: ProviderNodeTypes<DB = DatabaseEnv>>(
164        &self,
165        factory: ProviderFactory<N>,
166    ) -> eyre::Result<u64> {
167        let provider = factory.provider()?;
168        let last = provider.last_block_number()?;
169        let target = match self {
170            Self::ToBlock { target } => match target {
171                BlockHashOrNumber::Hash(hash) => provider
172                    .block_number(*hash)?
173                    .ok_or_else(|| eyre::eyre!("Block hash not found in database: {hash:?}"))?,
174                BlockHashOrNumber::Number(num) => *num,
175            },
176            Self::NumBlocks { amount } => last.saturating_sub(*amount),
177        };
178        if target > last {
179            eyre::bail!(
180                "Target block number {target} is higher than the latest block number {last}"
181            )
182        }
183        Ok(target)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use reth_chainspec::SEPOLIA;
191    use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
192
193    #[test]
194    fn parse_unwind() {
195        let cmd = Command::<EthereumChainSpecParser>::parse_from([
196            "reth",
197            "--datadir",
198            "dir",
199            "to-block",
200            "100",
201        ]);
202        assert_eq!(cmd.command, Subcommands::ToBlock { target: BlockHashOrNumber::Number(100) });
203
204        let cmd = Command::<EthereumChainSpecParser>::parse_from([
205            "reth",
206            "--datadir",
207            "dir",
208            "num-blocks",
209            "100",
210        ]);
211        assert_eq!(cmd.command, Subcommands::NumBlocks { amount: 100 });
212    }
213
214    #[test]
215    fn parse_unwind_chain() {
216        let cmd = Command::<EthereumChainSpecParser>::parse_from([
217            "reth", "--chain", "sepolia", "to-block", "100",
218        ]);
219        assert_eq!(cmd.command, Subcommands::ToBlock { target: BlockHashOrNumber::Number(100) });
220        assert_eq!(cmd.env.chain.chain_id(), SEPOLIA.chain_id());
221    }
222}