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 ) -> 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 let mut pipeline =
69 self.build_pipeline(config, provider_factory, components.evm_config().clone())?;
70
71 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 pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
142 Some(&self.env.chain)
143 }
144}
145
146#[derive(Subcommand, Debug, Eq, PartialEq)]
148enum Subcommands {
149 #[command(name = "to-block")]
152 ToBlock { target: BlockHashOrNumber },
153 #[command(name = "num-blocks")]
156 NumBlocks { amount: u64 },
157}
158
159impl Subcommands {
160 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}