Skip to main content

reth_cli_commands/
import_era.rs

1//! Command that initializes the node by importing a chain from ERA files.
2use crate::common::{AccessRights, CliNodeTypes, Environment, EnvironmentArgs};
3use alloy_chains::{ChainKind, NamedChain};
4use clap::{Args, Parser};
5use eyre::eyre;
6use reqwest::{Client, Url};
7use reth_chainspec::{EthChainSpec, EthereumHardforks};
8use reth_cli::chainspec::ChainSpecParser;
9use reth_era::common::file_ops::EraFileType;
10use reth_era_downloader::{read_dir, read_era_dir, EraClient, EraStream, EraStreamConfig};
11use reth_era_utils as era;
12use reth_etl::Collector;
13use reth_fs_util as fs;
14use reth_node_core::version::version_metadata;
15use reth_provider::StaticFileProviderFactory;
16use reth_static_file_types::StaticFileSegment;
17use std::{path::PathBuf, sync::Arc};
18use tracing::info;
19
20/// Syncs ERA encoded blocks from a local or remote source.
21#[derive(Debug, Parser)]
22pub struct ImportEraCommand<C: ChainSpecParser> {
23    #[command(flatten)]
24    env: EnvironmentArgs<C>,
25
26    #[clap(flatten)]
27    import: ImportArgs,
28
29    /// Stop the import after this block height has been reached.
30    ///
31    /// The file containing the block is imported up to and including this height, then the
32    /// import ends. By default all available blocks are imported.
33    #[arg(long, value_name = "TO_BLOCK", verbatim_doc_comment)]
34    to_block: Option<u64>,
35
36    /// Backfill the `Receipts` static file segment from its tip up to the Execution checkpoint.
37    ///
38    /// Only a missing tail is detected and repaired, never a gap inside the existing segment. The
39    /// backfill must reach the checkpoint exactly, so this cannot bootstrap a fresh database:
40    /// above the checkpoint receipts are pruned on the next node start, below it the node cannot
41    /// start. Byzantium onwards only, from `.era1` or `.ere` files that carry receipts.
42    #[arg(long, verbatim_doc_comment)]
43    with_receipts: bool,
44}
45
46#[derive(Debug, Args)]
47#[group(required = false, multiple = false)]
48pub struct ImportArgs {
49    /// The path to a directory for import.
50    ///
51    /// The ERA1 files are read from the local directory parsing headers and bodies.
52    #[arg(long, value_name = "IMPORT_ERA_PATH", verbatim_doc_comment)]
53    path: Option<PathBuf>,
54
55    /// The URL to a remote host where the ERA1 files are hosted.
56    ///
57    /// The ERA1 files are read from the remote host using HTTP GET requests parsing headers
58    /// and bodies.
59    #[arg(long, value_name = "IMPORT_ERA_URL", verbatim_doc_comment)]
60    url: Option<Url>,
61}
62
63trait TryFromChain {
64    fn try_to_url(&self) -> eyre::Result<Url>;
65}
66
67impl TryFromChain for ChainKind {
68    fn try_to_url(&self) -> eyre::Result<Url> {
69        Ok(match self {
70            ChainKind::Named(NamedChain::Mainnet) => {
71                Url::parse("https://era.ithaca.xyz/era1/index.html").expect("URL should be valid")
72            }
73            ChainKind::Named(NamedChain::Sepolia) => {
74                Url::parse("https://era.ithaca.xyz/sepolia-era1/index.html")
75                    .expect("URL should be valid")
76            }
77            chain => return Err(eyre!("No known host for ERA files on chain {chain:?}")),
78        })
79    }
80}
81
82impl<C: ChainSpecParser<ChainSpec: EthChainSpec + EthereumHardforks>> ImportEraCommand<C> {
83    /// Execute `import-era` command
84    pub async fn execute<N>(self, runtime: reth_tasks::Runtime) -> eyre::Result<()>
85    where
86        N: CliNodeTypes<ChainSpec = C::ChainSpec>,
87    {
88        info!(target: "reth::cli", "reth {} starting", version_metadata().short_version);
89
90        // Receipt backfill expects Receipts to trail Execution, so skip the normal consistency
91        // check that would unwind execution before the repair.
92        let access =
93            if self.with_receipts { AccessRights::RwInconsistent } else { AccessRights::RW };
94        let Environment { provider_factory, config, .. } = self.env.init::<N>(access, runtime)?;
95
96        let mut hash_collector = Collector::new(config.stages.etl.file_size, config.stages.etl.dir);
97
98        let static_file_provider = provider_factory.static_file_provider();
99        // The chain's first block, which is not necessarily 0: reth supports a non-zero genesis.
100        let genesis_block_number = static_file_provider.genesis_block_number();
101        let headers_tip = static_file_provider
102            .get_highest_static_file_block(StaticFileSegment::Headers)
103            .unwrap_or(genesis_block_number);
104
105        // With `--with-receipts`, resume from the receipts tip so files covering already-imported
106        // headers are re-read to backfill their receipts.
107        let resume_block = if self.with_receipts {
108            let receipts_tip = static_file_provider
109                .get_highest_static_file_block(StaticFileSegment::Receipts)
110                .unwrap_or(genesis_block_number);
111            headers_tip.min(receipts_tip)
112        } else {
113            headers_tip
114        };
115        let next_block = resume_block + 1;
116
117        // Pre-Byzantium receipts commit to a post-state root, so their receipts root can't be
118        // recomputed from what the node stores. The logs bloom is checked on every fork.
119        let chain_spec = self.env.chain.clone();
120        let is_receipt_verifiable =
121            move |number: u64| chain_spec.is_byzantium_active_at_block(number);
122
123        if let Some(path) = self.import.path {
124            let era_type = EraFileType::from_dir(&path)?.ok_or_else(|| {
125                eyre!(
126                    "No ERA (.era), ERA1 (.era1) or ERE (.ere, .erae) files found in {}",
127                    path.display()
128                )
129            })?;
130
131            info!(target: "reth::cli", ?era_type, path = %path.display(), to_block = ?self.to_block, with_receipts = self.with_receipts, "Starting ERA import");
132
133            check_receipts_supported(self.with_receipts, era_type)?;
134
135            match era_type {
136                EraFileType::Era => era::import::<era::Era, _, _, _, _, _, _>(
137                    read_era_dir(path)?,
138                    &provider_factory,
139                    &mut hash_collector,
140                    self.to_block,
141                    self.with_receipts,
142                    &is_receipt_verifiable,
143                )?,
144                EraFileType::Ere => era::import::<era::Ere, _, _, _, _, _, _>(
145                    read_dir(path, next_block)?,
146                    &provider_factory,
147                    &mut hash_collector,
148                    self.to_block,
149                    self.with_receipts,
150                    &is_receipt_verifiable,
151                )?,
152                EraFileType::Era1 => era::import::<era::Era1, _, _, _, _, _, _>(
153                    read_dir(path, next_block)?,
154                    &provider_factory,
155                    &mut hash_collector,
156                    self.to_block,
157                    self.with_receipts,
158                    &is_receipt_verifiable,
159                )?,
160            };
161        } else {
162            let url = match self.import.url {
163                Some(url) => url,
164                None => self.env.chain.chain().kind().try_to_url()?,
165            };
166            let era_type = EraFileType::from_url(url.as_str());
167
168            info!(target: "reth::cli", ?era_type, %url, to_block = ?self.to_block, with_receipts = self.with_receipts, "Starting ERA import");
169
170            check_receipts_supported(self.with_receipts, era_type)?;
171
172            let folder =
173                self.env.datadir.resolve_datadir(self.env.chain.chain()).data_dir().join("era");
174
175            fs::create_dir_all(&folder)?;
176
177            let mut config = EraStreamConfig::default();
178            // `start_from` maps a block number to a file index as `block / BLOCKS_PER_FILE`, valid
179            // only for execution-layer files (era1/ere). Consensus `.era` files are slot-indexed,
180            // so stream from 0 and let the pipeline skip already-imported blocks.
181            if !matches!(era_type, EraFileType::Era) {
182                config = config.start_from(next_block);
183            }
184            let client = EraClient::new(Client::new(), url, folder).with_era_type(era_type);
185            let stream = EraStream::new(client, config);
186
187            match era_type {
188                EraFileType::Ere => era::import::<era::Ere, _, _, _, _, _, _>(
189                    stream,
190                    &provider_factory,
191                    &mut hash_collector,
192                    self.to_block,
193                    self.with_receipts,
194                    &is_receipt_verifiable,
195                )?,
196                EraFileType::Era1 => era::import::<era::Era1, _, _, _, _, _, _>(
197                    stream,
198                    &provider_factory,
199                    &mut hash_collector,
200                    self.to_block,
201                    self.with_receipts,
202                    &is_receipt_verifiable,
203                )?,
204                EraFileType::Era => era::import::<era::Era, _, _, _, _, _, _>(
205                    stream,
206                    &provider_factory,
207                    &mut hash_collector,
208                    self.to_block,
209                    self.with_receipts,
210                    &is_receipt_verifiable,
211                )?,
212            };
213        }
214
215        Ok(())
216    }
217}
218
219/// Errors if `--with-receipts` was passed for the consensus `.era` format, which carries no
220/// receipt data at all.
221fn check_receipts_supported(with_receipts: bool, era_type: EraFileType) -> eyre::Result<()> {
222    if with_receipts && era_type == EraFileType::Era {
223        return Err(eyre!(
224            "--with-receipts is not supported for `.era` files: they contain no receipt data. \
225             Use `.era1` or `.ere` files instead."
226        ));
227    }
228    Ok(())
229}
230
231impl<C: ChainSpecParser> ImportEraCommand<C> {
232    /// Returns the underlying chain being used to run this command
233    pub fn chain_spec(&self) -> Option<&Arc<C::ChainSpec>> {
234        Some(&self.env.chain)
235    }
236}