1use crate::{StageCheckpoint, StageId};
2use alloy_primitives::{BlockHash, BlockNumber};
3use futures_util::{Stream, StreamExt};
4use reqwest::{Client, Url};
5use reth_config::config::EtlConfig;
6use reth_db_api::{table::Value, transaction::DbTxMut};
7use reth_era::{
8 common::file_ops::{EraFileType, StreamReader},
9 era::file::EraReader,
10 era1::file::Era1Reader,
11 ere::file::EreReader,
12};
13use reth_era_downloader::{read_dir, read_era_dir, EraClient, EraMeta, EraStream, EraStreamConfig};
14use reth_era_utils as era;
15use reth_etl::Collector;
16use reth_primitives_traits::{FullBlockBody, FullBlockHeader, NodePrimitives};
17use reth_provider::{
18 BlockReader, BlockWriter, DBProvider, StageCheckpointWriter, StaticFileProviderFactory,
19 StaticFileWriter,
20};
21use reth_stages_api::{ExecInput, ExecOutput, Stage, StageError, UnwindInput, UnwindOutput};
22use reth_static_file_types::StaticFileSegment;
23use std::{
24 fmt::{Debug, Formatter},
25 iter,
26 path::Path,
27 task::{ready, Context, Poll},
28};
29
30type Item<Header, Body> =
31 Box<dyn Iterator<Item = eyre::Result<(Header, Body)>> + Send + Sync + Unpin>;
32type ThreadSafeEraStream<Header, Body> =
33 Box<dyn Stream<Item = eyre::Result<Item<Header, Body>>> + Send + Sync + Unpin>;
34
35pub struct EraStage<Header, Body, StreamFactory> {
49 source: Option<StreamFactory>,
51 hash_collector: Collector<BlockHash, BlockNumber>,
54 item: Option<Item<Header, Body>>,
56 stream: Option<ThreadSafeEraStream<Header, Body>>,
58}
59
60trait EraStreamFactory<Header, Body> {
61 fn create(self, input: ExecInput) -> Result<ThreadSafeEraStream<Header, Body>, StageError>;
62}
63
64impl<Header, Body> EraStreamFactory<Header, Body> for EraImportSource
65where
66 Header: FullBlockHeader + Value,
67 Body: FullBlockBody<OmmerHeader = Header>,
68{
69 fn create(self, input: ExecInput) -> Result<ThreadSafeEraStream<Header, Body>, StageError> {
70 match self {
71 Self::Path(path) => {
72 if EraFileType::from_dir(&path).map_err(|e| StageError::Fatal(e.into()))? ==
75 Some(EraFileType::Era)
76 {
77 Self::convert(read_era_dir(path).map_err(|e| StageError::Fatal(e.into()))?)
78 } else {
79 Self::convert(
80 read_dir(path, input.next_block())
81 .map_err(|e| StageError::Fatal(e.into()))?,
82 )
83 }
84 }
85 Self::Url(url, folder) => {
86 let _ = reth_fs_util::create_dir_all(&folder);
87 let is_era = EraFileType::from_url(url.as_str()) == EraFileType::Era;
91 let client = EraClient::new(Client::new(), url, folder);
92 let mut config = EraStreamConfig::default();
93 if !is_era {
94 config = config.start_from(input.next_block());
95 }
96
97 Self::convert(EraStream::new(client, config))
98 }
99 }
100 }
101}
102
103impl EraImportSource {
104 fn convert<Header, Body>(
105 stream: impl Stream<Item = eyre::Result<impl EraMeta + Send + Sync + 'static + Unpin>>
106 + Send
107 + Sync
108 + 'static
109 + Unpin,
110 ) -> Result<ThreadSafeEraStream<Header, Body>, StageError>
111 where
112 Header: FullBlockHeader + Value,
113 Body: FullBlockBody<OmmerHeader = Header>,
114 {
115 Ok(Box::new(Box::pin(stream.map(|meta| {
116 meta.and_then(|meta| {
117 let file = reth_fs_util::open(meta.path())?;
120 let iter = match meta
121 .path()
122 .file_name()
123 .and_then(|name| name.to_str())
124 .and_then(EraFileType::from_filename)
125 {
126 Some(EraFileType::Era) => {
127 let mut buf = Vec::new();
130 Box::new(
131 EraReader::new(file).iter().filter_map(move |block| {
132 era::Era::decode(block, &mut buf).transpose()
133 }),
134 ) as Item<Header, Body>
135 }
136 Some(EraFileType::Ere) => {
137 Box::new(EreReader::new(file).iter().map(era::Ere::decode))
138 as Item<Header, Body>
139 }
140 _ => Box::new(Era1Reader::new(file).iter().map(era::decode))
141 as Item<Header, Body>,
142 };
143
144 let iter = iter.chain(
145 iter::once_with(move || match meta.mark_as_processed() {
146 Ok(..) => None,
147 Err(e) => Some(Err(e)),
148 })
149 .flatten(),
150 );
151
152 Ok(Box::new(iter) as Item<Header, Body>)
153 })
154 }))))
155 }
156}
157
158impl<Header: Debug, Body: Debug, F: Debug> Debug for EraStage<Header, Body, F> {
159 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
160 f.debug_struct("EraStage")
161 .field("source", &self.source)
162 .field("hash_collector", &self.hash_collector)
163 .field("item", &self.item.is_some())
164 .field("stream", &"dyn Stream")
165 .finish()
166 }
167}
168
169impl<Header, Body, F> EraStage<Header, Body, F> {
170 pub fn new(source: Option<F>, etl_config: EtlConfig) -> Self {
172 Self {
173 source,
174 item: None,
175 stream: None,
176 hash_collector: Collector::new(etl_config.file_size, etl_config.dir),
177 }
178 }
179}
180
181impl<Provider, N, F> Stage<Provider> for EraStage<N::BlockHeader, N::BlockBody, F>
182where
183 Provider: DBProvider<Tx: DbTxMut>
184 + StaticFileProviderFactory<Primitives = N>
185 + BlockWriter<Block = N::Block>
186 + BlockReader<Block = N::Block>
187 + StageCheckpointWriter,
188 F: EraStreamFactory<N::BlockHeader, N::BlockBody> + Send + Sync + Clone,
189 N: NodePrimitives<BlockHeader: Value>,
190{
191 fn id(&self) -> StageId {
192 StageId::Era
193 }
194
195 fn poll_execute_ready(
196 &mut self,
197 cx: &mut Context<'_>,
198 input: ExecInput,
199 ) -> Poll<Result<(), StageError>> {
200 if input.target_reached() || self.item.is_some() {
201 return Poll::Ready(Ok(()));
202 }
203
204 if self.stream.is_none() &&
205 let Some(source) = self.source.clone()
206 {
207 self.stream.replace(source.create(input)?);
208 }
209 if let Some(stream) = &mut self.stream &&
210 let Some(next) = ready!(stream.poll_next_unpin(cx))
211 .transpose()
212 .map_err(|e| StageError::Fatal(e.into()))?
213 {
214 self.item.replace(next);
215 }
216
217 Poll::Ready(Ok(()))
218 }
219
220 fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result<ExecOutput, StageError> {
221 let height = if let Some(era) = self.item.take() {
222 let static_file_provider = provider.static_file_provider();
223
224 let last_header_number = static_file_provider
227 .get_highest_static_file_block(StaticFileSegment::Headers)
228 .unwrap_or_default();
229
230 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers)?;
233
234 let height = era::process_iter(
236 era.map(|r| r.map(|(header, body)| (header, body, None))),
237 &mut writer,
238 None,
239 provider,
240 &mut self.hash_collector,
241 last_header_number..=input.target(),
242 era::ImportPolicy {
243 headers_tip: last_header_number,
244 is_receipt_verifiable: &|_| false,
245 },
246 )
247 .map_err(|e| StageError::Fatal(e.into()))?;
248
249 if !self.hash_collector.is_empty() {
250 era::build_index(provider, &mut self.hash_collector)
251 .map_err(|e| StageError::Recoverable(e.into()))?;
252 self.hash_collector.clear();
253 }
254
255 era::save_stage_checkpoints(
256 provider,
257 input.checkpoint().block_number,
258 height,
259 height,
260 input.target(),
261 )?;
262
263 height
264 } else {
265 let highest_header = provider
275 .static_file_provider()
276 .get_highest_static_file_block(StaticFileSegment::Headers)
277 .unwrap_or_default();
278
279 let checkpoint = input.checkpoint().block_number;
280 let from_target = input.target.unwrap_or(checkpoint);
281
282 checkpoint.max(highest_header).max(from_target)
283 };
284
285 Ok(ExecOutput { checkpoint: StageCheckpoint::new(height), done: height >= input.target() })
286 }
287
288 fn unwind(
289 &mut self,
290 _provider: &Provider,
291 input: UnwindInput,
292 ) -> Result<UnwindOutput, StageError> {
293 Ok(UnwindOutput { checkpoint: input.checkpoint.with_block_number(input.unwind_to) })
294 }
295}
296
297#[derive(Debug, Clone)]
299pub enum EraImportSource {
300 Url(Url, Box<Path>),
302 Path(Box<Path>),
304}
305
306impl EraImportSource {
307 pub fn maybe_new(
319 path: Option<Box<Path>>,
320 url: Option<Url>,
321 default: impl FnOnce() -> Option<Url>,
322 folder: impl FnOnce() -> Box<Path>,
323 ) -> Option<Self> {
324 path.map(Self::Path).or_else(|| url.or_else(default).map(|url| Self::Url(url, folder())))
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::test_utils::{
332 stage_test_suite, ExecuteStageTestRunner, StageTestRunner, UnwindStageTestRunner,
333 };
334 use alloy_consensus::{BlockBody, Header};
335 use alloy_primitives::B256;
336 use assert_matches::assert_matches;
337 use futures_util::stream;
338 use reth_db_api::tables;
339 use reth_era::{
340 common::file_ops::{EraFileFormat, StreamWriter},
341 ere::{
342 file::{EreFile, EreWriter},
343 types::{
344 execution::{BlockTuple, CompressedBody, CompressedHeader},
345 group::{DynamicBlockIndex, EreGroup, EreId},
346 },
347 },
348 };
349 use reth_ethereum_primitives::{Block, TransactionSigned};
350 use reth_primitives_traits::SealedBlock;
351 use reth_provider::BlockHashReader;
352 use reth_testing_utils::generators::{
353 self, random_block_range, random_header, BlockRangeParams,
354 };
355 use std::{fs::File, path::PathBuf};
356 use test_runner::EraTestRunner;
357
358 #[tokio::test]
359 async fn test_era_range_ends_below_target() {
360 let era_cap = 2;
361 let target = 20000;
362
363 let mut runner = EraTestRunner::default();
364
365 let input = ExecInput { target: Some(era_cap), checkpoint: None };
366 runner.seed_execution(input).unwrap();
367
368 let input = ExecInput { target: Some(target), checkpoint: None };
369 let output = runner.execute(input).await.unwrap();
370
371 runner.commit();
372
373 assert_matches!(
374 output,
375 Ok(ExecOutput {
376 checkpoint: StageCheckpoint { block_number, stage_checkpoint: None },
377 done: false
378 }) if block_number == era_cap
379 );
380
381 let output = output.unwrap();
382 let validation_output = runner.validate_execution(input, Some(output.clone()));
383
384 assert_matches!(validation_output, Ok(()));
385
386 runner.take_responses();
387
388 let input = ExecInput { target: Some(target), checkpoint: Some(output.checkpoint) };
389 let output = runner.execute(input).await.unwrap();
390
391 runner.commit();
392
393 assert_matches!(
394 output,
395 Ok(ExecOutput {
396 checkpoint: StageCheckpoint { block_number, stage_checkpoint: None },
397 done: true
398 }) if block_number == target
399 );
400
401 let validation_output = runner.validate_execution(input, output.ok());
402
403 assert_matches!(validation_output, Ok(()));
404 }
405
406 fn write_ere_file(path: &Path, blocks: &[SealedBlock<Block>]) {
408 let tuples = blocks
409 .iter()
410 .map(|block| {
411 BlockTuple::new(
412 CompressedHeader::from_header(block.header()).unwrap(),
413 CompressedBody::from_body(block.body()).unwrap(),
414 )
415 })
416 .collect::<Vec<_>>();
417
418 let start = blocks[0].number;
419 let component_count = 2;
422 let offsets = vec![0; blocks.len() * component_count as usize];
423 let index = DynamicBlockIndex::new(start, component_count, offsets);
424 let group = EreGroup::new(tuples, None, index);
425 let file = EreFile::new(group, EreId::new("mainnet", start, blocks.len() as u32));
426
427 EreWriter::new(File::create(path).unwrap()).write_file(&file).unwrap();
428 }
429
430 #[derive(Debug)]
431 struct EreTestMeta {
432 path: PathBuf,
433 }
434
435 impl EraMeta for EreTestMeta {
436 fn mark_as_processed(&self) -> eyre::Result<()> {
437 Ok(())
438 }
439
440 fn path(&self) -> &Path {
441 &self.path
442 }
443 }
444
445 #[tokio::test]
446 async fn convert_decodes_ere_files() {
447 let mut rng = generators::rng();
448 let blocks = random_block_range(
449 &mut rng,
450 1..=3,
451 BlockRangeParams { tx_count: 1..3, ..Default::default() },
452 );
453
454 let dir = tempfile::tempdir().unwrap();
455
456 for ext in ["ere", "erae"] {
459 let path = dir.path().join(format!("mainnet-00000-abcd1234.{ext}"));
460 write_ere_file(&path, &blocks);
461
462 let stream =
463 stream::iter(vec![Ok::<_, eyre::Error>(EreTestMeta { path: path.clone() })]);
464 let mut stream =
465 EraImportSource::convert::<Header, BlockBody<TransactionSigned>>(stream).unwrap();
466
467 let item = stream.next().await.expect("a file to decode").expect("decoding to succeed");
468 let decoded = item.collect::<eyre::Result<Vec<_>>>().unwrap();
469
470 assert_eq!(
471 decoded.len(),
472 blocks.len(),
473 "ERE file with `.{ext}` extension should decode every block"
474 );
475 for ((header, body), block) in decoded.iter().zip(&blocks) {
476 assert_eq!(header, block.header());
477 assert_eq!(body, block.body());
478 }
479 }
480 }
481
482 mod test_runner {
483 use super::*;
484 use crate::test_utils::{TestRunnerError, TestStageDB};
485 use alloy_consensus::{BlockBody, Header};
486 use futures_util::stream;
487 use reth_db_api::{
488 cursor::DbCursorRO,
489 models::{StoredBlockBodyIndices, StoredBlockOmmers},
490 transaction::DbTx,
491 };
492 use reth_ethereum_primitives::TransactionSigned;
493 use reth_primitives_traits::{SealedBlock, SealedHeader};
494 use reth_provider::{BlockNumReader, HeaderProvider, TransactionsProvider};
495 use reth_testing_utils::generators::{
496 random_block_range, random_signed_tx, BlockRangeParams,
497 };
498 use tokio::sync::watch;
499
500 pub(crate) struct EraTestRunner {
501 channel: (watch::Sender<B256>, watch::Receiver<B256>),
502 db: TestStageDB,
503 responses: Option<Vec<(Header, BlockBody<TransactionSigned>)>>,
504 }
505
506 impl Default for EraTestRunner {
507 fn default() -> Self {
508 Self {
509 channel: watch::channel(B256::ZERO),
510 db: TestStageDB::default(),
511 responses: Default::default(),
512 }
513 }
514 }
515
516 impl StageTestRunner for EraTestRunner {
517 type S = EraStage<Header, BlockBody<TransactionSigned>, StubResponses>;
518
519 fn db(&self) -> &TestStageDB {
520 &self.db
521 }
522
523 fn stage(&self) -> Self::S {
524 EraStage::new(self.responses.clone().map(StubResponses), EtlConfig::default())
525 }
526 }
527
528 impl ExecuteStageTestRunner for EraTestRunner {
529 type Seed = Vec<SealedBlock<reth_ethereum_primitives::Block>>;
530
531 fn seed_execution(&mut self, input: ExecInput) -> Result<Self::Seed, TestRunnerError> {
532 let start = input.checkpoint().block_number;
533 let end = input.target();
534
535 let static_file_provider = self.db.factory.static_file_provider();
536
537 let mut rng = generators::rng();
538
539 let blocks = random_block_range(
541 &mut rng,
542 0..=end,
543 BlockRangeParams {
544 parent: Some(B256::ZERO),
545 tx_count: 0..2,
546 ..Default::default()
547 },
548 );
549 self.db.insert_headers(blocks.iter().map(|block| block.sealed_header()))?;
550 if let Some(progress) = blocks.get(start as usize) {
551 {
553 let tx = self.db.factory.provider_rw()?.into_tx();
554 let mut static_file_producer = static_file_provider
555 .get_writer(start, StaticFileSegment::Transactions)?;
556
557 let body = StoredBlockBodyIndices {
558 first_tx_num: 0,
559 tx_count: progress.transaction_count() as u64,
560 };
561
562 static_file_producer.set_block_range(0..=progress.number);
563
564 body.tx_num_range().try_for_each(|tx_num| {
565 let transaction = random_signed_tx(&mut rng);
566 static_file_producer.append_transaction(tx_num, &transaction).map(drop)
567 })?;
568
569 if body.tx_count != 0 {
570 tx.put::<tables::TransactionBlocks>(
571 body.last_tx_num(),
572 progress.number,
573 )?;
574 }
575
576 tx.put::<tables::BlockBodyIndices>(progress.number, body)?;
577
578 if !progress.ommers_hash_is_empty() {
579 tx.put::<tables::BlockOmmers>(
580 progress.number,
581 StoredBlockOmmers { ommers: progress.body().ommers.clone() },
582 )?;
583 }
584
585 static_file_producer.commit()?;
586 tx.commit()?;
587 }
588 }
589 self.responses.replace(
590 blocks.iter().map(|v| (v.header().clone(), v.body().clone())).collect(),
591 );
592 Ok(blocks)
593 }
594
595 fn validate_execution(
597 &self,
598 input: ExecInput,
599 output: Option<ExecOutput>,
600 ) -> Result<(), TestRunnerError> {
601 let initial_checkpoint = input.checkpoint().block_number;
602 match output {
603 Some(output) if output.checkpoint.block_number > initial_checkpoint => {
604 let provider = self.db.factory.provider()?;
605
606 for block_num in initial_checkpoint..
607 output
608 .checkpoint
609 .block_number
610 .min(self.responses.as_ref().map(|v| v.len()).unwrap_or_default()
611 as BlockNumber)
612 {
613 let hash = provider.block_hash(block_num)?.expect("no header hash");
615
616 assert_eq!(provider.block_number(hash)?, Some(block_num));
618
619 let header = provider.header_by_number(block_num)?;
621 assert!(header.is_some());
622 let header = SealedHeader::seal_slow(header.unwrap());
623 assert_eq!(header.hash(), hash);
624 }
625
626 self.validate_db_blocks(
627 output.checkpoint.block_number,
628 output.checkpoint.block_number,
629 )?;
630 }
631 _ => self.check_no_header_entry_above(initial_checkpoint)?,
632 };
633 Ok(())
634 }
635
636 async fn after_execution(&self, headers: Self::Seed) -> Result<(), TestRunnerError> {
637 let tip = if headers.is_empty() {
638 let tip = random_header(&mut generators::rng(), 0, None);
639 self.db.insert_headers(iter::once(&tip))?;
640 tip.hash()
641 } else {
642 headers.last().unwrap().hash()
643 };
644 self.send_tip(tip);
645 Ok(())
646 }
647 }
648
649 impl UnwindStageTestRunner for EraTestRunner {
650 fn validate_unwind(&self, _input: UnwindInput) -> Result<(), TestRunnerError> {
651 Ok(())
652 }
653 }
654
655 impl EraTestRunner {
656 pub(crate) fn check_no_header_entry_above(
657 &self,
658 block: BlockNumber,
659 ) -> Result<(), TestRunnerError> {
660 self.db
661 .ensure_no_entry_above_by_value::<tables::HeaderNumbers, _>(block, |val| val)?;
662 self.db.ensure_no_entry_above::<tables::CanonicalHeaders, _>(block, |key| key)?;
663 self.db.ensure_no_entry_above::<tables::Headers, _>(block, |key| key)?;
664 Ok(())
665 }
666
667 pub(crate) fn send_tip(&self, tip: B256) {
668 self.channel.0.send(tip).expect("failed to send tip");
669 }
670
671 pub(crate) fn validate_db_blocks(
673 &self,
674 prev_progress: BlockNumber,
675 highest_block: BlockNumber,
676 ) -> Result<(), TestRunnerError> {
677 let static_file_provider = self.db.factory.static_file_provider();
678
679 self.db.query(|tx| {
680 let mut bodies_cursor = tx.cursor_read::<tables::BlockBodyIndices>()?;
682 let mut ommers_cursor = tx.cursor_read::<tables::BlockOmmers>()?;
683 let mut tx_block_cursor = tx.cursor_read::<tables::TransactionBlocks>()?;
684
685 let first_body_key = match bodies_cursor.first()? {
686 Some((key, _)) => key,
687 None => return Ok(()),
688 };
689
690 let mut prev_number: Option<BlockNumber> = None;
691
692
693 for entry in bodies_cursor.walk(Some(first_body_key))? {
694 let (number, body) = entry?;
695
696 if number > prev_progress
699 && let Some(prev_key) = prev_number {
700 assert_eq!(prev_key + 1, number, "Body entries must be sequential");
701 }
702
703 assert!(
705 number <= highest_block,
706 "We wrote a block body outside of our synced range. Found block with number {number}, highest block according to stage is {highest_block}",
707 );
708
709 let header = static_file_provider.header_by_number(number)?.expect("to be present");
710 let stored_ommers = ommers_cursor.seek_exact(number)?;
712 if header.ommers_hash_is_empty() {
713 assert!(stored_ommers.is_none(), "Unexpected ommers entry");
714 } else {
715 assert!(stored_ommers.is_some(), "Missing ommers entry");
716 }
717
718 let tx_block_id = tx_block_cursor.seek_exact(body.last_tx_num())?.map(|(_,b)| b);
719 if body.tx_count == 0 {
720 assert_ne!(tx_block_id,Some(number));
721 } else {
722 assert_eq!(tx_block_id, Some(number));
723 }
724
725 for tx_id in body.tx_num_range() {
726 assert!(static_file_provider.transaction_by_id(tx_id)?.is_some(), "Transaction is missing.");
727 }
728
729 prev_number = Some(number);
730 }
731 Ok(())
732 })?;
733 Ok(())
734 }
735
736 pub(crate) fn take_responses(&mut self) {
737 self.responses.take();
738 }
739
740 pub(crate) fn commit(&self) {
741 self.db.factory.static_file_provider().commit().unwrap();
742 }
743 }
744
745 #[derive(Clone)]
746 pub(crate) struct StubResponses(Vec<(Header, BlockBody<TransactionSigned>)>);
747
748 impl EraStreamFactory<Header, BlockBody<TransactionSigned>> for StubResponses {
749 fn create(
750 self,
751 _input: ExecInput,
752 ) -> Result<ThreadSafeEraStream<Header, BlockBody<TransactionSigned>>, StageError>
753 {
754 let stream = stream::iter(vec![self.0]);
755
756 Ok(Box::new(Box::pin(stream.map(|meta| {
757 Ok(Box::new(meta.into_iter().map(Ok))
758 as Item<Header, BlockBody<TransactionSigned>>)
759 }))))
760 }
761 }
762 }
763
764 stage_test_suite!(EraTestRunner, era);
765}