1use 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#[derive(Debug, Parser)]
31pub struct Command<C: ChainSpecParser> {
32 #[command(flatten)]
33 env: EnvironmentArgs<C>,
34
35 #[command(subcommand)]
36 command: Subcommands,
37
38 #[arg(long)]
41 offline: bool,
42}
43
44impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> Command<C> {
45 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 let mut pipeline =
71 self.build_pipeline(config, provider_factory, components.evm_config().clone())?;
72
73 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 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
149 Some(&self.env.chain)
150 }
151}
152
153#[derive(Subcommand, Debug, Eq, PartialEq)]
155enum Subcommands {
156 #[command(name = "to-block")]
159 ToBlock { target: BlockHashOrNumber },
160 #[command(name = "num-blocks")]
163 NumBlocks { amount: u64 },
164}
165
166impl Subcommands {
167 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}