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    ) -> eyre::Result<()>
50    where
51        Comp: CliNodeComponents<N>,
52        F: FnOnce(Arc<C::ChainSpec>) -> Comp,
53    {
54        let Environment { provider_factory, config, .. } = self.env.init::<N>(AccessRights::RW)?;
55
56        let target = self.command.unwind_target(provider_factory.clone())?;
57
58        let components = components(provider_factory.chain_spec());
59
60        if self.offline {
61            info!(target: "reth::cli", "Performing an unwind for offline-only data!");
62        }
63
64        let highest_static_file_block = provider_factory.provider()?.last_block_number()?;
65        info!(target: "reth::cli", ?target, ?highest_static_file_block, prune_config=?config.prune,  "Executing a pipeline unwind.");
66
67        // This will build an offline-only pipeline if the `offline` flag is enabled
68        let mut pipeline =
69            self.build_pipeline(config, provider_factory, components.evm_config().clone())?;
70
71        // Move all applicable data from database to static files.
72        pipeline.move_to_static_files()?;
73
74        pipeline.unwind(target, None)?;
75
76        info!(target: "reth::cli", ?target, "Unwound blocks");
77
78        Ok(())
79    }
80
81    fn build_pipeline<N: ProviderNodeTypes<ChainSpec = C::ChainSpec>>(
82        self,
83        config: Config,
84        provider_factory: ProviderFactory<N>,
85        evm_config: impl ConfigureEvm<Primitives = N::Primitives> + 'static,
86    ) -> Result<Pipeline<N>, eyre::Error> {
87        let stage_conf = &config.stages;
88        let prune_modes = config.prune.segments.clone();
89
90        let (tip_tx, tip_rx) = watch::channel(B256::ZERO);
91
92        let builder = if self.offline {
93            Pipeline::<N>::builder().add_stages(
94                OfflineStages::new(
95                    evm_config,
96                    NoopConsensus::arc(),
97                    config.stages,
98                    prune_modes.clone(),
99                )
100                .builder()
101                .disable(reth_stages::StageId::SenderRecovery),
102            )
103        } else {
104            Pipeline::<N>::builder().with_tip_sender(tip_tx).add_stages(
105                DefaultStages::new(
106                    provider_factory.clone(),
107                    tip_rx,
108                    Arc::new(NoopConsensus::default()),
109                    NoopHeaderDownloader::default(),
110                    NoopBodiesDownloader::default(),
111                    evm_config.clone(),
112                    stage_conf.clone(),
113                    prune_modes.clone(),
114                    None,
115                )
116                .set(ExecutionStage::new(
117                    evm_config,
118                    Arc::new(NoopConsensus::default()),
119                    ExecutionStageThresholds {
120                        max_blocks: None,
121                        max_changes: None,
122                        max_cumulative_gas: None,
123                        max_duration: None,
124                    },
125                    stage_conf.execution_external_clean_threshold(),
126                    ExExManagerHandle::empty(),
127                )),
128            )
129        };
130
131        let pipeline = builder.build(
132            provider_factory.clone(),
133            StaticFileProducer::new(provider_factory, prune_modes),
134        );
135        Ok(pipeline)
136    }
137}
138
139impl<C: ChainSpecParser> Command<C> {
140    /// Return the underlying chain being used to run this command
141    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
142        Some(&self.env.chain)
143    }
144}
145
146/// `reth stage unwind` subcommand
147#[derive(Subcommand, Debug, Eq, PartialEq)]
148enum Subcommands {
149    /// Unwinds the database from the latest block, until the given block number or hash has been
150    /// reached, that block is not included.
151    #[command(name = "to-block")]
152    ToBlock { target: BlockHashOrNumber },
153    /// Unwinds the database from the latest block, until the given number of blocks have been
154    /// reached.
155    #[command(name = "num-blocks")]
156    NumBlocks { amount: u64 },
157}
158
159impl Subcommands {
160    /// Returns the block to unwind to. The returned block will stay in database.
161    fn unwind_target<N: ProviderNodeTypes<DB = Arc<DatabaseEnv>>>(
162        &self,
163        factory: ProviderFactory<N>,
164    ) -> eyre::Result<u64> {
165        let provider = factory.provider()?;
166        let last = provider.last_block_number()?;
167        let target = match self {
168            Self::ToBlock { target } => match target {
169                BlockHashOrNumber::Hash(hash) => provider
170                    .block_number(*hash)?
171                    .ok_or_else(|| eyre::eyre!("Block hash not found in database: {hash:?}"))?,
172                BlockHashOrNumber::Number(num) => *num,
173            },
174            Self::NumBlocks { amount } => last.saturating_sub(*amount),
175        };
176        if target > last {
177            eyre::bail!(
178                "Target block number {target} is higher than the latest block number {last}"
179            )
180        }
181        Ok(target)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use reth_chainspec::SEPOLIA;
189    use reth_ethereum_cli::chainspec::EthereumChainSpecParser;
190
191    #[test]
192    fn parse_unwind() {
193        let cmd = Command::<EthereumChainSpecParser>::parse_from([
194            "reth",
195            "--datadir",
196            "dir",
197            "to-block",
198            "100",
199        ]);
200        assert_eq!(cmd.command, Subcommands::ToBlock { target: BlockHashOrNumber::Number(100) });
201
202        let cmd = Command::<EthereumChainSpecParser>::parse_from([
203            "reth",
204            "--datadir",
205            "dir",
206            "num-blocks",
207            "100",
208        ]);
209        assert_eq!(cmd.command, Subcommands::NumBlocks { amount: 100 });
210    }
211
212    #[test]
213    fn parse_unwind_chain() {
214        let cmd = Command::<EthereumChainSpecParser>::parse_from([
215            "reth", "--chain", "sepolia", "to-block", "100",
216        ]);
217        assert_eq!(cmd.command, Subcommands::ToBlock { target: BlockHashOrNumber::Number(100) });
218        assert_eq!(cmd.env.chain.chain_id(), SEPOLIA.chain_id());
219    }
220}