1use alloy_consensus::BlockHeader;
2use alloy_primitives::{BlockHash, BlockNumber, U256};
3use futures_util::{Stream, StreamExt};
4use reth_db_api::{
5 cursor::{DbCursorRO, DbCursorRW},
6 table::Value,
7 tables,
8 transaction::{DbTx, DbTxMut},
9 RawKey, RawTable, RawValue,
10};
11use reth_era::{
12 common::{decode::DecodeCompressedRlp, file_ops::StreamReader},
13 e2s::error::E2sError,
14 era::{file::EraReader, types::consensus::CompressedSignedBeaconBlock},
15 era1::{file::Era1Reader, types::execution::BlockTuple},
16 ere::{file::EreReader, types::execution::BlockTuple as EreBlockTuple},
17};
18use reth_era_downloader::EraMeta;
19use reth_etl::Collector;
20use reth_fs_util as fs;
21use reth_primitives_traits::{Block, BlockBody, FullBlockBody, FullBlockHeader, NodePrimitives};
22use reth_provider::{
23 providers::StaticFileProviderRWRefMut, BlockReader, BlockWriter, StaticFileProviderFactory,
24 StaticFileSegment, StaticFileWriter,
25};
26use reth_stages_types::{
27 CheckpointBlockRange, EntitiesCheckpoint, HeadersCheckpoint, StageCheckpoint, StageId,
28};
29use reth_storage_api::{
30 errors::ProviderResult, DBProvider, DatabaseProviderFactory, NodePrimitivesProvider,
31 StageCheckpointWriter,
32};
33use std::{collections::Bound, error::Error, ops::RangeBounds, sync::mpsc};
34use tracing::info;
35
36pub trait EraBlockReader<BH, BB> {
40 fn blocks<M: EraMeta + ?Sized>(
42 meta: &M,
43 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(BH, BB)>>>;
44}
45
46#[derive(Debug)]
48pub struct Era1;
49
50impl<BH, BB> EraBlockReader<BH, BB> for Era1
51where
52 BH: FullBlockHeader + Value,
53 BB: FullBlockBody<OmmerHeader = BH>,
54{
55 fn blocks<M: EraMeta + ?Sized>(
56 meta: &M,
57 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(BH, BB)>>> {
58 let reader: Era1Reader<std::fs::File> = open(meta)?;
59 Ok(reader.iter().map(decode::<BH, BB, E2sError>))
60 }
61}
62
63impl<BH, BB> EraBlockReader<BH, BB> for Ere
64where
65 BH: FullBlockHeader + Value,
66 BB: FullBlockBody<OmmerHeader = BH>,
67{
68 fn blocks<M: EraMeta + ?Sized>(
69 meta: &M,
70 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(BH, BB)>>> {
71 let reader: EreReader<std::fs::File> = open(meta)?;
72 Ok(reader.iter().map(Self::decode))
73 }
74}
75
76#[derive(Debug)]
78pub struct Ere;
79
80impl Ere {
81 pub fn decode<BH, BB, E>(block: Result<EreBlockTuple, E>) -> eyre::Result<(BH, BB)>
84 where
85 BH: FullBlockHeader + Value,
86 BB: FullBlockBody<OmmerHeader = BH>,
87 E: From<E2sError> + Error + Send + Sync + 'static,
88 {
89 let block = block?;
90 let header: BH = block.header.decode()?;
91 let body: BB = block.body.decode()?;
92 Ok((header, body))
93 }
94}
95
96#[derive(Debug)]
103pub struct Era;
104
105impl<BH, BB> EraBlockReader<BH, BB> for Era
106where
107 BH: FullBlockHeader,
108 BB: FullBlockBody,
109{
110 fn blocks<M: EraMeta + ?Sized>(
111 meta: &M,
112 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(BH, BB)>>> {
113 let reader: EraReader<std::fs::File> = open(meta)?;
114 let mut buf = Vec::new();
115 Ok(reader.iter().filter_map(move |block| Self::decode(block, &mut buf).transpose()))
116 }
117}
118
119impl Era {
120 pub fn decode<BH, BB>(
124 block: Result<CompressedSignedBeaconBlock, E2sError>,
125 buf: &mut Vec<u8>,
126 ) -> eyre::Result<Option<(BH, BB)>>
127 where
128 BH: FullBlockHeader,
129 BB: FullBlockBody,
130 {
131 let Some(alloy_consensus::Block { header, body }) =
132 block?.decode_execution_block::<<BB as BlockBody>::Transaction>()?
133 else {
134 return Ok(None);
135 };
136 Ok(Some((reencode_rlp(&header, buf)?, reencode_rlp(&body, buf)?)))
139 }
140}
141
142fn reencode_rlp<T, U>(value: &T, buf: &mut Vec<u8>) -> eyre::Result<U>
148where
149 T: alloy_rlp::Encodable,
150 U: alloy_rlp::Decodable,
151{
152 buf.clear();
153 alloy_rlp::Encodable::encode(value, buf);
154 Ok(<U as alloy_rlp::Decodable>::decode(&mut buf.as_slice())?)
155}
156
157pub fn open<Reader>(meta: &(impl EraMeta + ?Sized)) -> eyre::Result<Reader>
159where
160 Reader: StreamReader<std::fs::File>,
161{
162 Ok(Reader::new(fs::open(meta.path())?))
163}
164
165pub fn import<S, Downloader, Era, PF, B, BB, BH>(
172 mut downloader: Downloader,
173 provider_factory: &PF,
174 hash_collector: &mut Collector<BlockHash, BlockNumber>,
175 to_block: Option<BlockNumber>,
176) -> eyre::Result<BlockNumber>
177where
178 S: EraBlockReader<BH, BB>,
179 B: Block<Header = BH, Body = BB>,
180 BH: FullBlockHeader + Value,
181 BB: FullBlockBody<
182 Transaction = <<<PF as DatabaseProviderFactory>::ProviderRW as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
183 OmmerHeader = BH,
184 >,
185 Downloader: Stream<Item = eyre::Result<Era>> + Send + 'static + Unpin,
186 Era: EraMeta + Send + 'static,
187 PF: DatabaseProviderFactory<
188 ProviderRW: BlockWriter<Block = B>
189 + DBProvider
190 + StaticFileProviderFactory<Primitives: NodePrimitives<Block = B, BlockHeader = BH, BlockBody = BB>>
191 + StageCheckpointWriter,
192 > + StaticFileProviderFactory<Primitives = <<PF as DatabaseProviderFactory>::ProviderRW as NodePrimitivesProvider>::Primitives>,
193{
194 let (tx, rx) = mpsc::channel();
195
196 tokio::spawn(async move {
198 while let Some(file) = downloader.next().await {
199 tx.send(Some(file))?;
200 }
201 tx.send(None)
202 });
203
204 let static_file_provider = provider_factory.static_file_provider();
205
206 let mut height = static_file_provider
209 .get_highest_static_file_block(StaticFileSegment::Headers)
210 .unwrap_or_default();
211
212 let end = to_block.map_or(Bound::Unbounded, Bound::Included);
213
214 while let Some(meta) = rx.recv()? {
215 let meta = meta?;
216 let from = height;
217 let provider = provider_factory.database_provider_rw()?;
218
219 height = process::<S, _, _, _, _>(
220 &meta,
221 &mut static_file_provider.latest_writer(StaticFileSegment::Headers)?,
222 &provider,
223 hash_collector,
224 (Bound::Included(height), end),
225 )?;
226
227 save_stage_checkpoints(&provider, from, height, height, height)?;
228
229 provider.commit()?;
230
231 info!(target: "era::history::import", first = from, last = height, file = %meta.path().display(), "Imported ERA file");
232
233 if to_block.is_some_and(|to| height >= to) {
234 break;
235 }
236 }
237
238 let provider = provider_factory.database_provider_rw()?;
239
240 build_index(&provider, hash_collector)?;
241
242 provider.commit()?;
243
244 Ok(height)
245}
246
247pub fn save_stage_checkpoints<P>(
253 provider: P,
254 from: BlockNumber,
255 to: BlockNumber,
256 processed: u64,
257 total: u64,
258) -> ProviderResult<()>
259where
260 P: StageCheckpointWriter,
261{
262 provider.save_stage_checkpoint(
263 StageId::Headers,
264 StageCheckpoint::new(to).with_headers_stage_checkpoint(HeadersCheckpoint {
265 block_range: CheckpointBlockRange { from, to },
266 progress: EntitiesCheckpoint { processed, total },
267 }),
268 )?;
269 provider.save_stage_checkpoint(
270 StageId::Bodies,
271 StageCheckpoint::new(to)
272 .with_entities_stage_checkpoint(EntitiesCheckpoint { processed, total }),
273 )?;
274 Ok(())
275}
276
277pub fn process<S, P, B, BB, BH>(
280 meta: &(impl EraMeta + ?Sized),
281 writer: &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
282 provider: &P,
283 hash_collector: &mut Collector<BlockHash, BlockNumber>,
284 block_numbers: impl RangeBounds<BlockNumber>,
285) -> eyre::Result<BlockNumber>
286where
287 S: EraBlockReader<BH, BB>,
288 B: Block<Header = BH, Body = BB>,
289 BH: FullBlockHeader + Value,
290 BB: FullBlockBody<
291 Transaction = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
292 OmmerHeader = BH,
293 >,
294 P: DBProvider<Tx: DbTxMut> + NodePrimitivesProvider + BlockWriter<Block = B>,
295 <P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
296{
297 let iter = S::blocks(meta)?
298 .map(Some)
299 .chain(std::iter::once_with(|| match meta.mark_as_processed() {
300 Ok(()) => None,
301 Err(error) => Some(Err(error)),
302 }))
303 .flatten();
304
305 process_iter(iter, writer, provider, hash_collector, block_numbers)
306}
307
308pub fn decode<BH, BB, E>(block: Result<BlockTuple, E>) -> eyre::Result<(BH, BB)>
310where
311 BH: FullBlockHeader + Value,
312 BB: FullBlockBody<OmmerHeader = BH>,
313 E: From<E2sError> + Error + Send + Sync + 'static,
314{
315 let block = block?;
316 let header: BH = block.header.decode()?;
317 let body: BB = block.body.decode()?;
318
319 Ok((header, body))
320}
321
322pub fn process_iter<P, B, BB, BH>(
334 mut iter: impl Iterator<Item = eyre::Result<(BH, BB)>>,
335 writer: &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
336 provider: &P,
337 hash_collector: &mut Collector<BlockHash, BlockNumber>,
338 block_numbers: impl RangeBounds<BlockNumber>,
339) -> eyre::Result<BlockNumber>
340where
341 B: Block<Header = BH, Body = BB>,
342 BH: FullBlockHeader + Value,
343 BB: FullBlockBody<
344 Transaction = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
345 OmmerHeader = BH,
346 >,
347 P: DBProvider<Tx: DbTxMut> + NodePrimitivesProvider + BlockWriter<Block = B>,
348 <P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
349{
350 let mut last_header_number = match block_numbers.start_bound() {
351 Bound::Included(&number) => number,
352 Bound::Excluded(&number) => number.saturating_add(1),
353 Bound::Unbounded => 0,
354 };
355 let target = match block_numbers.end_bound() {
356 Bound::Included(&number) => Some(number),
357 Bound::Excluded(&number) => Some(number.saturating_sub(1)),
358 Bound::Unbounded => None,
359 };
360
361 for block in &mut iter {
362 let (header, body) = block?;
363 let number = header.number();
364
365 if number <= last_header_number {
366 continue;
367 }
368 if let Some(target) = target &&
369 number > target
370 {
371 break;
372 }
373
374 if number != last_header_number + 1 {
377 eyre::bail!(
378 "non-contiguous ERA import: expected block {}, got {number}; the execution \
379 database must be synced up to block {} before importing this file",
380 last_header_number + 1,
381 number - 1,
382 );
383 }
384
385 let hash = header.hash_slow();
386 last_header_number = number;
387
388 writer.append_header(&header, &hash)?;
390
391 provider.append_block_bodies(vec![(header.number(), Some(&body))])?;
393
394 hash_collector.insert(hash, number)?;
395 }
396
397 Ok(last_header_number)
398}
399
400pub fn build_index<P>(
402 provider: &P,
403 hash_collector: &mut Collector<BlockHash, BlockNumber>,
404) -> eyre::Result<()>
405where
406 P: DBProvider<Tx: DbTxMut>,
407{
408 let total_headers = hash_collector.len();
409 info!(target: "era::history::import", total = total_headers, "Writing headers hash index");
410
411 let mut cursor_header_numbers =
413 provider.tx_ref().cursor_write::<RawTable<tables::HeaderNumbers>>()?;
414 let first_sync = if provider.tx_ref().entries::<RawTable<tables::HeaderNumbers>>()? == 1 &&
417 let Some((hash, block_number)) = cursor_header_numbers.last()? &&
418 block_number.value()? == 0
419 {
420 hash_collector.insert(hash.key()?, 0)?;
421 cursor_header_numbers.delete_current()?;
422 true
423 } else {
424 false
425 };
426
427 let interval = (total_headers / 10).max(8192);
428
429 for (index, hash_to_number) in hash_collector.iter()?.enumerate() {
431 let (hash, number) = hash_to_number?;
432
433 if index != 0 && index.is_multiple_of(interval) {
434 info!(target: "era::history::import", progress = %format!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers hash index");
435 }
436
437 let hash = RawKey::<BlockHash>::from_vec(hash);
438 let number = RawValue::<BlockNumber>::from_vec(number);
439
440 if first_sync {
441 cursor_header_numbers.append(hash, &number)?;
442 } else {
443 cursor_header_numbers.upsert(hash, &number)?;
444 }
445 }
446
447 Ok(())
448}
449
450pub fn calculate_td_by_number<P>(provider: &P, num: BlockNumber) -> eyre::Result<U256>
457where
458 P: BlockReader,
459{
460 let mut total_difficulty = U256::ZERO;
461 let mut start = 0;
462
463 while start <= num {
464 let end = (start + 1000 - 1).min(num);
465
466 total_difficulty +=
467 provider.headers_range(start..=end)?.iter().map(|h| h.difficulty()).sum::<U256>();
468
469 start = end + 1;
470 }
471
472 Ok(total_difficulty)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use alloy_consensus::Header;
479 use reth_db_common::init::init_genesis;
480 use reth_ethereum_primitives::{Block, BlockBody};
481 use reth_provider::{
482 test_utils::create_test_provider_factory, DatabaseProviderFactory,
483 StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
484 };
485 use std::{cell::Cell, path::Path};
486 use tempfile::tempdir;
487
488 struct TestEra;
489
490 impl EraBlockReader<Header, BlockBody> for TestEra {
491 fn blocks<M: EraMeta + ?Sized>(
492 _meta: &M,
493 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody)>>> {
494 Ok([1, 2]
495 .into_iter()
496 .map(|number| Ok((Header { number, ..Default::default() }, BlockBody::default()))))
497 }
498 }
499
500 #[derive(Debug)]
501 struct TestMeta {
502 marked: Cell<bool>,
503 }
504
505 impl EraMeta for TestMeta {
506 fn mark_as_processed(&self) -> eyre::Result<()> {
507 self.marked.set(true);
508 Ok(())
509 }
510
511 fn path(&self) -> &Path {
512 Path::new("test.era1")
513 }
514 }
515
516 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
517 async fn import_stops_at_to_block() {
518 let pf = create_test_provider_factory();
519 init_genesis(&pf).unwrap();
520
521 let folder = tempdir().unwrap();
522 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
523
524 let stream = futures_util::stream::iter(vec![
526 Ok(TestMeta { marked: Cell::new(false) }),
527 Ok(TestMeta { marked: Cell::new(false) }),
528 ]);
529
530 let height =
531 import::<TestEra, _, _, _, Block, _, _>(stream, &pf, &mut hash_collector, Some(1))
532 .unwrap();
533
534 assert_eq!(height, 1);
535 }
536
537 #[test]
538 fn process_does_not_mark_partially_consumed_file_processed() {
539 let pf = create_test_provider_factory();
540 init_genesis(&pf).unwrap();
541
542 let static_file_provider = pf.static_file_provider();
543 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
544 let provider = pf.database_provider_rw().unwrap();
545 let folder = tempdir().unwrap();
546 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
547 let meta = TestMeta { marked: Cell::new(false) };
548
549 let height = process::<TestEra, _, Block, _, _>(
550 &meta,
551 &mut writer,
552 &provider,
553 &mut hash_collector,
554 0..=1,
555 )
556 .unwrap();
557
558 assert_eq!(height, 1);
559 assert!(!meta.marked.get());
560 }
561
562 #[test]
563 fn process_iter_rejects_non_contiguous_blocks() {
564 let pf = create_test_provider_factory();
565 init_genesis(&pf).unwrap();
566
567 let static_file_provider = pf.static_file_provider();
568 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
569 let provider = pf.database_provider_rw().unwrap();
570 let folder = tempdir().unwrap();
571 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
572
573 let blocks = [5u64, 6]
576 .into_iter()
577 .map(|number| Ok((Header { number, ..Default::default() }, BlockBody::default())));
578
579 let result = process_iter::<_, Block, _, _>(
580 blocks,
581 &mut writer,
582 &provider,
583 &mut hash_collector,
584 0..,
585 );
586
587 assert!(result.is_err());
588 }
589}