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::ExecutionStage,
22 ExecutionStageThresholds, Pipeline, 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 .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 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
144 Some(&self.env.chain)
145 }
146}
147
148#[derive(Subcommand, Debug, Eq, PartialEq)]
150enum Subcommands {
151 #[command(name = "to-block")]
154 ToBlock { target: BlockHashOrNumber },
155 #[command(name = "num-blocks")]
158 NumBlocks { amount: u64 },
159}
160
161impl Subcommands {
162 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}