1use alloy_consensus::{
2 proofs::calculate_receipt_root, BlockHeader, Eip658Value, ReceiptEnvelope, ReceiptWithBloom,
3 RlpDecodableReceipt, TxReceipt,
4};
5use alloy_primitives::{BlockHash, BlockNumber, Bloom, U256};
6use alloy_rlp::Decodable;
7use futures_util::{Stream, StreamExt};
8use reth_codecs::Compact;
9use reth_db_api::{
10 cursor::{DbCursorRO, DbCursorRW},
11 table::Value,
12 tables,
13 transaction::{DbTx, DbTxMut},
14 RawKey, RawTable, RawValue,
15};
16use reth_era::{
17 common::{decode::DecodeCompressedRlp, file_ops::StreamReader},
18 e2s::error::E2sError,
19 era::{file::EraReader, types::consensus::CompressedSignedBeaconBlock},
20 era1::{file::Era1Reader, types::execution::BlockTuple},
21 ere::{file::EreReader, types::execution::BlockTuple as EreBlockTuple},
22};
23use reth_era_downloader::EraMeta;
24use reth_etl::Collector;
25use reth_fs_util as fs;
26use reth_primitives_traits::{
27 Block, BlockBody, FullBlockBody, FullBlockHeader, NodePrimitives, Receipt,
28};
29use reth_provider::{
30 providers::StaticFileProviderRWRefMut, BlockReader, BlockWriter, EitherWriter,
31 EitherWriterDestination, StaticFileProviderFactory, StaticFileSegment, StaticFileWriter,
32};
33use reth_stages_types::{
34 CheckpointBlockRange, EntitiesCheckpoint, HeadersCheckpoint, StageCheckpoint, StageId,
35};
36use reth_storage_api::{
37 errors::{ProviderError, ProviderResult},
38 BlockBodyIndicesProvider, BlockHashReader, DBProvider, DatabaseProviderFactory,
39 NodePrimitivesProvider, StageCheckpointReader, StageCheckpointWriter, StorageSettingsCache,
40};
41use std::{collections::Bound, error::Error, ops::RangeBounds, sync::mpsc};
42use tracing::info;
43
44type EraBlock<BH, BB, R> = (BH, BB, Option<Vec<R>>);
46
47type ReceiptOf<P> = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::Receipt;
49
50pub trait EraBlockReader<BH, BB, R> {
57 fn blocks<M: EraMeta + ?Sized>(
59 meta: &M,
60 decode_receipts: bool,
61 ) -> eyre::Result<impl Iterator<Item = eyre::Result<EraBlock<BH, BB, R>>>>;
62}
63
64#[derive(Debug)]
66pub struct Era1;
67
68impl<BH, BB, R> EraBlockReader<BH, BB, R> for Era1
69where
70 BH: FullBlockHeader + Value,
71 BB: FullBlockBody<OmmerHeader = BH>,
72 R: RlpDecodableReceipt,
73{
74 fn blocks<M: EraMeta + ?Sized>(
75 meta: &M,
76 decode_receipts: bool,
77 ) -> eyre::Result<impl Iterator<Item = eyre::Result<EraBlock<BH, BB, R>>>> {
78 let reader: Era1Reader<std::fs::File> = open(meta)?;
79 Ok(reader
80 .iter()
81 .map(move |block| decode_with_receipts::<BH, BB, R, E2sError>(block, decode_receipts)))
82 }
83}
84
85impl<BH, BB, R> EraBlockReader<BH, BB, R> for Ere
86where
87 BH: FullBlockHeader + Value,
88 BB: FullBlockBody<OmmerHeader = BH>,
89 R: RlpDecodableReceipt,
90{
91 fn blocks<M: EraMeta + ?Sized>(
92 meta: &M,
93 decode_receipts: bool,
94 ) -> eyre::Result<impl Iterator<Item = eyre::Result<EraBlock<BH, BB, R>>>> {
95 let reader: EreReader<std::fs::File> = open(meta)?;
96 Ok(reader.iter().map(move |block| Self::decode_with_receipts(block, decode_receipts)))
97 }
98}
99
100#[derive(Debug)]
102pub struct Ere;
103
104impl Ere {
105 pub fn decode<BH, BB, E>(block: Result<EreBlockTuple, E>) -> eyre::Result<(BH, BB)>
109 where
110 BH: FullBlockHeader + Value,
111 BB: FullBlockBody<OmmerHeader = BH>,
112 E: From<E2sError> + Error + Send + Sync + 'static,
113 {
114 let block = block?;
115 let header: BH = block.header.decode()?;
116 let body: BB = block.body.decode()?;
117 Ok((header, body))
118 }
119
120 pub fn decode_with_receipts<BH, BB, R, E>(
124 block: Result<EreBlockTuple, E>,
125 decode_receipts: bool,
126 ) -> eyre::Result<EraBlock<BH, BB, R>>
127 where
128 BH: FullBlockHeader + Value,
129 BB: FullBlockBody<OmmerHeader = BH>,
130 R: RlpDecodableReceipt,
131 E: From<E2sError> + Error + Send + Sync + 'static,
132 {
133 let block = block?;
134 let header: BH = block.header.decode()?;
135 let body: BB = block.body.decode()?;
136 let number = header.number();
137 let receipts = decode_receipts
138 .then(|| block.receipts.as_ref().map(|r| r.decode_receipts()))
139 .flatten()
140 .transpose()?
141 .map(|slim| receipts_from_envelopes(number, slim.into_iter().map(Into::into).collect()))
142 .transpose()?;
143
144 Ok((header, body, receipts))
145 }
146}
147
148#[derive(Debug)]
156pub struct Era;
157
158impl<BH, BB, R> EraBlockReader<BH, BB, R> for Era
159where
160 BH: FullBlockHeader,
161 BB: FullBlockBody,
162{
163 fn blocks<M: EraMeta + ?Sized>(
165 meta: &M,
166 _decode_receipts: bool,
167 ) -> eyre::Result<impl Iterator<Item = eyre::Result<EraBlock<BH, BB, R>>>> {
168 let reader: EraReader<std::fs::File> = open(meta)?;
169 let mut buf = Vec::new();
170 Ok(reader.iter().filter_map(move |block| {
171 Self::decode(block, &mut buf)
172 .map(|opt| opt.map(|(header, body)| (header, body, None)))
173 .transpose()
174 }))
175 }
176}
177
178impl Era {
179 pub fn decode<BH, BB>(
183 block: Result<CompressedSignedBeaconBlock, E2sError>,
184 buf: &mut Vec<u8>,
185 ) -> eyre::Result<Option<(BH, BB)>>
186 where
187 BH: FullBlockHeader,
188 BB: FullBlockBody,
189 {
190 let Some(alloy_consensus::Block { header, body }) =
191 block?.decode_execution_block::<<BB as BlockBody>::Transaction>()?
192 else {
193 return Ok(None);
194 };
195 Ok(Some((reencode_rlp(&header, buf)?, reencode_rlp(&body, buf)?)))
198 }
199}
200
201fn reencode_rlp<T, U>(value: &T, buf: &mut Vec<u8>) -> eyre::Result<U>
207where
208 T: alloy_rlp::Encodable,
209 U: alloy_rlp::Decodable,
210{
211 buf.clear();
212 alloy_rlp::Encodable::encode(value, buf);
213 Ok(<U as alloy_rlp::Decodable>::decode(&mut buf.as_slice())?)
214}
215
216pub fn open<Reader>(meta: &(impl EraMeta + ?Sized)) -> eyre::Result<Reader>
218where
219 Reader: StreamReader<std::fs::File>,
220{
221 Ok(Reader::new(fs::open(meta.path())?))
222}
223
224pub fn import<S, Downloader, Era, PF, B, BB, BH>(
239 mut downloader: Downloader,
240 provider_factory: &PF,
241 hash_collector: &mut Collector<BlockHash, BlockNumber>,
242 to_block: Option<BlockNumber>,
243 store_receipts: bool,
244 is_receipt_verifiable: &dyn Fn(BlockNumber) -> bool,
245) -> eyre::Result<BlockNumber>
246where
247 S: EraBlockReader<BH, BB, ReceiptOf<<PF as DatabaseProviderFactory>::ProviderRW>>,
248 B: Block<Header = BH, Body = BB>,
249 BH: FullBlockHeader + Value,
250 BB: FullBlockBody<
251 Transaction = <<<PF as DatabaseProviderFactory>::ProviderRW as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
252 OmmerHeader = BH,
253 >,
254 Downloader: Stream<Item = eyre::Result<Era>> + Send + 'static + Unpin,
255 Era: EraMeta + Send + 'static,
256 PF: DatabaseProviderFactory<
257 ProviderRW: BlockWriter<Block = B>
258 + DBProvider
259 + BlockBodyIndicesProvider
260 + BlockHashReader
261 + StaticFileProviderFactory<Primitives: NodePrimitives<Block = B, BlockHeader = BH, BlockBody = BB>>
262 + StageCheckpointReader
263 + StageCheckpointWriter
264 + StorageSettingsCache,
265 > + StaticFileProviderFactory<Primitives = <<PF as DatabaseProviderFactory>::ProviderRW as NodePrimitivesProvider>::Primitives>,
266 ReceiptOf<<PF as DatabaseProviderFactory>::ProviderRW>: Compact + Receipt,
267{
268 let (tx, rx) = mpsc::channel();
269
270 tokio::spawn(async move {
272 while let Some(file) = downloader.next().await {
273 tx.send(Some(file))?;
274 }
275 tx.send(None)
276 });
277
278 let static_file_provider = provider_factory.static_file_provider();
279
280 let genesis_block_number = static_file_provider.genesis_block_number();
282
283 let headers_tip = static_file_provider
286 .get_highest_static_file_block(StaticFileSegment::Headers)
287 .unwrap_or(genesis_block_number);
288
289 let receipts_tip = store_receipts
292 .then(|| static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts));
293 let mut height = match receipts_tip {
294 Some(tip) => headers_tip.min(tip.unwrap_or(genesis_block_number)),
295 None => headers_tip,
296 };
297
298 let receipts_target = store_receipts
301 .then(|| -> eyre::Result<_> {
302 let provider = provider_factory.database_provider_rw()?;
303
304 if !matches!(
307 EitherWriter::receipts_destination(&provider),
308 EitherWriterDestination::StaticFile
309 ) {
310 eyre::bail!(
311 "receipt import writes the Receipts static file segment, but this node's \
312 prune configuration keeps receipts elsewhere. Remove the receipt pruning \
313 configuration, or import without receipts"
314 );
315 }
316
317 let target = provider
318 .get_stage_checkpoint(StageId::Execution)?
319 .map(|checkpoint| checkpoint.block_number)
320 .unwrap_or(genesis_block_number);
321
322 if target <= genesis_block_number {
323 eyre::bail!(
324 "receipt import repairs the receipt static files of an already-executed \
325 range, but this database has executed no blocks. Sync the node first, or \
326 import without receipts"
327 );
328 }
329 if height >= target {
330 eyre::bail!(
331 "receipts already cover the executed range up to block {target}, nothing to \
332 repair"
333 );
334 }
335 if let Some(to_block) = to_block &&
336 to_block != target
337 {
338 eyre::bail!(
339 "--to-block {to_block} does not match the Execution checkpoint {target}. A \
340 receipt repair must cover the executed range exactly, so either drop \
341 --to-block or set it to {target}"
342 );
343 }
344 if !is_receipt_verifiable(height + 1) {
347 eyre::bail!(
348 "receipt repair would start at block {}, which predates Byzantium. Those \
349 receipts commit to a post-state root this node's receipt type cannot \
350 represent, so the Receipts segment has to already cover the pre-Byzantium \
351 range",
352 height + 1,
353 );
354 }
355
356 Ok(target)
357 })
358 .transpose()?;
359
360 if matches!(receipts_tip, Some(None)) {
365 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Receipts)?;
366 writer.user_header_mut().set_block_range(genesis_block_number, genesis_block_number);
367 writer.commit()?;
368 }
369
370 let to_block = receipts_target.or(to_block);
371 let end = to_block.map_or(Bound::Unbounded, Bound::Included);
372 let policy = ImportPolicy { headers_tip, is_receipt_verifiable };
373
374 while let Some(meta) = rx.recv()? {
375 let meta = meta?;
376 let from = height;
377 let provider = provider_factory.database_provider_rw()?;
378
379 let mut receipts_writer = store_receipts
382 .then(|| static_file_provider.latest_writer(StaticFileSegment::Receipts))
383 .transpose()?;
384
385 height = process::<S, _, _, _, _>(
386 &meta,
387 &mut static_file_provider.latest_writer(StaticFileSegment::Headers)?,
388 receipts_writer.as_mut(),
389 &provider,
390 hash_collector,
391 (Bound::Included(height), end),
392 policy,
393 )?;
394
395 drop(receipts_writer);
398
399 let checkpoint = height.max(headers_tip);
402 save_stage_checkpoints(&provider, from, checkpoint, checkpoint, checkpoint)?;
403
404 provider.commit()?;
405
406 info!(target: "era::history::import", first = from, last = height, file = %meta.path().display(), "Imported ERA file");
407
408 if to_block.is_some_and(|to| height >= to) {
409 break;
410 }
411 }
412
413 let provider = provider_factory.database_provider_rw()?;
414
415 build_index(&provider, hash_collector)?;
416
417 provider.commit()?;
418
419 if let Some(target) = receipts_target &&
422 height < target
423 {
424 eyre::bail!(
425 "receipt repair reached block {height} but the executed range ends at {target}. The \
426 source ran out of files, re-run with the files covering blocks {}..={target}",
427 height + 1,
428 );
429 }
430
431 Ok(height)
432}
433
434pub fn save_stage_checkpoints<P>(
439 provider: P,
440 from: BlockNumber,
441 to: BlockNumber,
442 processed: u64,
443 total: u64,
444) -> ProviderResult<()>
445where
446 P: StageCheckpointWriter,
447{
448 provider.save_stage_checkpoint(
449 StageId::Headers,
450 StageCheckpoint::new(to).with_headers_stage_checkpoint(HeadersCheckpoint {
451 block_range: CheckpointBlockRange { from, to },
452 progress: EntitiesCheckpoint { processed, total },
453 }),
454 )?;
455 provider.save_stage_checkpoint(
456 StageId::Bodies,
457 StageCheckpoint::new(to)
458 .with_entities_stage_checkpoint(EntitiesCheckpoint { processed, total }),
459 )?;
460 Ok(())
461}
462
463pub fn process<S, P, B, BB, BH>(
466 meta: &(impl EraMeta + ?Sized),
467 writer: &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
468 receipts_writer: Option<
469 &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
470 >,
471 provider: &P,
472 hash_collector: &mut Collector<BlockHash, BlockNumber>,
473 block_numbers: impl RangeBounds<BlockNumber>,
474 policy: ImportPolicy<'_>,
475) -> eyre::Result<BlockNumber>
476where
477 S: EraBlockReader<BH, BB, ReceiptOf<P>>,
478 B: Block<Header = BH, Body = BB>,
479 BH: FullBlockHeader + Value,
480 BB: FullBlockBody<
481 Transaction = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
482 OmmerHeader = BH,
483 >,
484 P: DBProvider<Tx: DbTxMut>
485 + NodePrimitivesProvider
486 + BlockWriter<Block = B>
487 + BlockBodyIndicesProvider
488 + BlockHashReader,
489 <P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
490 ReceiptOf<P>: Compact + Receipt,
491{
492 let decode_receipts = receipts_writer.is_some();
493 let iter = S::blocks(meta, decode_receipts)?
494 .map(Some)
495 .chain(std::iter::once_with(|| match meta.mark_as_processed() {
496 Ok(()) => None,
497 Err(error) => Some(Err(error)),
498 }))
499 .flatten();
500
501 process_iter(iter, writer, receipts_writer, provider, hash_collector, block_numbers, policy)
502}
503
504#[derive(Clone, Copy)]
506pub struct ImportPolicy<'a> {
507 pub headers_tip: BlockNumber,
509 pub is_receipt_verifiable: &'a dyn Fn(BlockNumber) -> bool,
511}
512
513impl std::fmt::Debug for ImportPolicy<'_> {
514 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
515 f.debug_struct("ImportPolicy")
517 .field("headers_tip", &self.headers_tip)
518 .finish_non_exhaustive()
519 }
520}
521
522pub fn decode<BH, BB, E>(block: Result<BlockTuple, E>) -> eyre::Result<(BH, BB)>
524where
525 BH: FullBlockHeader + Value,
526 BB: FullBlockBody<OmmerHeader = BH>,
527 E: From<E2sError> + Error + Send + Sync + 'static,
528{
529 let block = block?;
530 let header: BH = block.header.decode()?;
531 let body: BB = block.body.decode()?;
532 Ok((header, body))
533}
534
535pub fn decode_with_receipts<BH, BB, R, E>(
539 block: Result<BlockTuple, E>,
540 decode_receipts: bool,
541) -> eyre::Result<EraBlock<BH, BB, R>>
542where
543 BH: FullBlockHeader + Value,
544 BB: FullBlockBody<OmmerHeader = BH>,
545 R: RlpDecodableReceipt,
546 E: From<E2sError> + Error + Send + Sync + 'static,
547{
548 let block = block?;
549 let header: BH = block.header.decode()?;
550 let body: BB = block.body.decode()?;
551 let number = header.number();
552 let receipts = decode_receipts
553 .then(|| -> eyre::Result<_> {
554 match block.receipts.decode::<Vec<ReceiptWithBloom<R>>>() {
555 Ok(receipts) => {
556 Ok(receipts.into_iter().map(|with_bloom| with_bloom.receipt).collect())
557 }
558 Err(err) => match block.receipts.decode::<Vec<ReceiptEnvelope>>() {
561 Ok(envelopes) => receipts_from_envelopes(number, envelopes),
562 Err(_) => Err(err.into()),
563 },
564 }
565 })
566 .transpose()?;
567
568 Ok((header, body, receipts))
569}
570
571fn receipts_from_envelopes<R: RlpDecodableReceipt>(
578 number: BlockNumber,
579 envelopes: Vec<ReceiptEnvelope>,
580) -> eyre::Result<Vec<R>> {
581 for envelope in &envelopes {
582 if matches!(envelope.status_or_post_state(), Eip658Value::PostState(_)) {
583 eyre::bail!(
584 "block {number} has pre-Byzantium receipts, which commit to a post-state root \
585 rather than a success status and so cannot be represented by this node's receipt \
586 type. Receipt import is only supported from Byzantium onwards"
587 );
588 }
589 }
590
591 let encoded = alloy_rlp::encode(&envelopes);
592
593 Ok(Vec::<ReceiptWithBloom<R>>::decode(&mut encoded.as_slice())?
594 .into_iter()
595 .map(|with_bloom| with_bloom.receipt)
596 .collect())
597}
598
599pub fn process_iter<P, B, BB, BH>(
620 mut iter: impl Iterator<Item = eyre::Result<EraBlock<BH, BB, ReceiptOf<P>>>>,
621 writer: &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
622 mut receipts_writer: Option<
623 &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
624 >,
625 provider: &P,
626 hash_collector: &mut Collector<BlockHash, BlockNumber>,
627 block_numbers: impl RangeBounds<BlockNumber>,
628 policy: ImportPolicy<'_>,
629) -> eyre::Result<BlockNumber>
630where
631 B: Block<Header = BH, Body = BB>,
632 BH: FullBlockHeader + Value,
633 BB: FullBlockBody<
634 Transaction = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
635 OmmerHeader = BH,
636 >,
637 P: DBProvider<Tx: DbTxMut>
638 + NodePrimitivesProvider
639 + BlockWriter<Block = B>
640 + BlockBodyIndicesProvider
641 + BlockHashReader,
642 <P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
643 ReceiptOf<P>: Compact + Receipt,
644{
645 let mut last_header_number = match block_numbers.start_bound() {
646 Bound::Included(&number) => number,
647 Bound::Excluded(&number) => number.saturating_add(1),
648 Bound::Unbounded => 0,
649 };
650 let target = match block_numbers.end_bound() {
651 Bound::Included(&number) => Some(number),
652 Bound::Excluded(&number) => Some(number.saturating_sub(1)),
653 Bound::Unbounded => None,
654 };
655
656 for block in &mut iter {
657 let (header, body, receipts) = block?;
658 let number = header.number();
659
660 if number <= last_header_number {
661 continue;
662 }
663 if let Some(target) = target &&
664 number > target
665 {
666 break;
667 }
668
669 if number != last_header_number + 1 {
672 eyre::bail!(
673 "non-contiguous ERA import: expected block {}, got {number}; the execution \
674 database must be synced up to block {} before importing this file",
675 last_header_number + 1,
676 number - 1,
677 );
678 }
679
680 last_header_number = number;
681
682 if number > policy.headers_tip {
685 let hash = header.hash_slow();
686 writer.append_header(&header, &hash)?;
687 provider.append_block_bodies(vec![(header.number(), Some(&body))])?;
688 hash_collector.insert(hash, number)?;
689 } else if provider.block_hash(number)? != Some(header.hash_slow()) {
690 eyre::bail!(
693 "block {number} in this ERA file does not match the block already imported at \
694 that height"
695 );
696 }
697
698 if let Some(receipts_writer) = receipts_writer.as_deref_mut() {
699 if let Some(receipts) = receipts.as_deref() {
700 verify_receipts(&header, receipts, (policy.is_receipt_verifiable)(number))?;
701 }
702 provider.write_block_receipts(receipts_writer, number, receipts)?;
703 }
704 }
705
706 Ok(last_header_number)
707}
708
709fn verify_receipts<BH, R>(header: &BH, receipts: &[R], check_root: bool) -> eyre::Result<()>
714where
715 BH: FullBlockHeader,
716 R: Receipt,
717{
718 let with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
719 let logs_bloom = with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
720
721 if logs_bloom != header.logs_bloom() {
722 eyre::bail!("logs bloom mismatch for block {}", header.number());
723 }
724
725 if check_root {
726 let receipts_root = calculate_receipt_root(&with_bloom);
727 if receipts_root != header.receipts_root() {
728 eyre::bail!(
729 "receipts root mismatch for block {}: computed {receipts_root}, header has {}",
730 header.number(),
731 header.receipts_root(),
732 );
733 }
734 }
735
736 Ok(())
737}
738
739trait BlockReceiptsWriterExt: BlockBodyIndicesProvider {
742 fn write_block_receipts<N: NodePrimitives>(
747 &self,
748 receipts_writer: &mut StaticFileProviderRWRefMut<'_, N>,
749 number: BlockNumber,
750 receipts: Option<Vec<N::Receipt>>,
751 ) -> eyre::Result<()>
752 where
753 N::Receipt: Compact;
754}
755
756impl<P: BlockBodyIndicesProvider> BlockReceiptsWriterExt for P {
757 fn write_block_receipts<N: NodePrimitives>(
758 &self,
759 receipts_writer: &mut StaticFileProviderRWRefMut<'_, N>,
760 number: BlockNumber,
761 receipts: Option<Vec<N::Receipt>>,
762 ) -> eyre::Result<()>
763 where
764 N::Receipt: Compact,
765 {
766 let Some(block_receipts) = receipts else {
767 eyre::bail!(
768 "block {number} has no receipts in the imported ERA file; drop --with-receipts, \
769 or import files that carry receipts for every block (`.era1` always includes \
770 them; `.ere` receipts are optional per spec)"
771 );
772 };
773
774 let indices = self
775 .block_body_indices(number)?
776 .ok_or_else(|| eyre::eyre!("missing block body indices for block {number}"))?;
777
778 if block_receipts.len() as u64 != indices.tx_count {
779 eyre::bail!(
780 "receipt count mismatch for block {number}: {} receipt(s) for {} transaction(s)",
781 block_receipts.len(),
782 indices.tx_count,
783 );
784 }
785
786 receipts_writer.increment_block(number)?;
787 receipts_writer.append_receipts(
788 (indices.first_tx_num..).zip(block_receipts.iter()).map(Ok::<_, ProviderError>),
789 )?;
790
791 Ok(())
792 }
793}
794
795pub fn build_index<P>(
797 provider: &P,
798 hash_collector: &mut Collector<BlockHash, BlockNumber>,
799) -> eyre::Result<()>
800where
801 P: DBProvider<Tx: DbTxMut>,
802{
803 let total_headers = hash_collector.len();
804 info!(target: "era::history::import", total = total_headers, "Writing headers hash index");
805
806 let mut cursor_header_numbers =
808 provider.tx_ref().cursor_write::<RawTable<tables::HeaderNumbers>>()?;
809 let first_sync = if provider.tx_ref().entries::<RawTable<tables::HeaderNumbers>>()? == 1 &&
812 let Some((hash, block_number)) = cursor_header_numbers.last()? &&
813 block_number.value()? == 0
814 {
815 hash_collector.insert(hash.key()?, 0)?;
816 cursor_header_numbers.delete_current()?;
817 true
818 } else {
819 false
820 };
821
822 let interval = (total_headers / 10).max(8192);
823
824 for (index, hash_to_number) in hash_collector.iter()?.enumerate() {
826 let (hash, number) = hash_to_number?;
827
828 if index != 0 && index.is_multiple_of(interval) {
829 info!(target: "era::history::import", progress = %format!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers hash index");
830 }
831
832 let hash = RawKey::<BlockHash>::from_vec(hash);
833 let number = RawValue::<BlockNumber>::from_vec(number);
834
835 if first_sync {
836 cursor_header_numbers.append(hash, &number)?;
837 } else {
838 cursor_header_numbers.upsert(hash, &number)?;
839 }
840 }
841
842 Ok(())
843}
844
845pub fn calculate_td_by_number<P>(provider: &P, num: BlockNumber) -> eyre::Result<U256>
852where
853 P: BlockReader,
854{
855 let mut total_difficulty = U256::ZERO;
856 let mut start = 0;
857
858 while start <= num {
859 let end = (start + 1000 - 1).min(num);
860
861 total_difficulty +=
862 provider.headers_range(start..=end)?.iter().map(|h| h.difficulty()).sum::<U256>();
863
864 start = end + 1;
865 }
866
867 Ok(total_difficulty)
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873 use alloy_consensus::{Header, Receipt as RlpReceipt, ReceiptWithBloom, TxLegacy, TxType};
874 use alloy_primitives::{Address, Bytes, Log, Signature, B256};
875 use reth_db_common::init::init_genesis;
876 use reth_era::{
877 era1::types::execution::{
878 CompressedBody, CompressedHeader, CompressedReceipts, TotalDifficulty,
879 },
880 ere::types::execution::{
881 CompressedBody as EreCompressedBody, CompressedHeader as EreCompressedHeader,
882 CompressedSlimReceipts, SlimReceipt,
883 },
884 };
885 use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
886 use reth_provider::{
887 test_utils::{
888 create_test_provider_factory, create_test_provider_factory_with_genesis_block_number,
889 },
890 DatabaseProviderFactory, ReceiptProvider, StageCheckpointReader, StaticFileProviderFactory,
891 StaticFileSegment, StaticFileWriter,
892 };
893 use reth_prune_types::{PruneMode, PruneModes, ReceiptsLogPruneConfig};
894 use std::{cell::Cell, path::Path};
895 use tempfile::tempdir;
896
897 fn era1_block_tuple(number: u64, receipts: Vec<ReceiptEnvelope>) -> BlockTuple {
899 let header = Header { number, ..Default::default() };
900 BlockTuple::new(
901 CompressedHeader::from_rlp(&alloy_rlp::encode(&header)).unwrap(),
902 CompressedBody::from_rlp(&alloy_rlp::encode(BlockBody::default())).unwrap(),
903 CompressedReceipts::from_rlp(&alloy_rlp::encode(&receipts)).unwrap(),
904 TotalDifficulty::new(U256::ZERO),
905 )
906 }
907
908 fn block_with_one_receipt(number: u64) -> (Header, BlockBody, Receipt) {
910 let tx = TransactionSigned::new_unhashed(
911 TxLegacy::default().into(),
912 Signature::test_signature(),
913 );
914 let receipt = Receipt {
915 tx_type: TxType::Legacy,
916 success: true,
917 cumulative_gas_used: 21_000,
918 logs: vec![],
919 };
920
921 let with_bloom = vec![TxReceipt::with_bloom_ref(&receipt)];
922 let header = Header {
923 number,
924 receipts_root: calculate_receipt_root(&with_bloom),
925 logs_bloom: with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref()),
926 ..Default::default()
927 };
928
929 (header, BlockBody { transactions: vec![tx], ..Default::default() }, receipt)
930 }
931
932 fn era1_receipt(status: Eip658Value) -> ReceiptEnvelope {
934 ReceiptEnvelope::Legacy(ReceiptWithBloom::new(
935 RlpReceipt { status, cumulative_gas_used: 21_000, logs: vec![] },
936 Bloom::ZERO,
937 ))
938 }
939
940 fn ere_block_tuple(number: u64, receipts: &[SlimReceipt]) -> EreBlockTuple {
942 let header = Header { number, ..Default::default() };
943 EreBlockTuple::new(
944 EreCompressedHeader::from_rlp(&alloy_rlp::encode(&header)).unwrap(),
945 EreCompressedBody::from_rlp(&alloy_rlp::encode(BlockBody::default())).unwrap(),
946 )
947 .with_receipts(CompressedSlimReceipts::from_receipts(receipts).unwrap())
948 }
949
950 struct TestEra;
951
952 impl<R> EraBlockReader<Header, BlockBody, R> for TestEra {
953 fn blocks<M: EraMeta + ?Sized>(
954 _meta: &M,
955 _decode_receipts: bool,
956 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
957 {
958 Ok([1, 2].into_iter().map(|number| {
959 Ok((Header { number, ..Default::default() }, BlockBody::default(), None))
960 }))
961 }
962 }
963
964 struct TestEraWithEmptyReceipts;
966
967 impl<R> EraBlockReader<Header, BlockBody, R> for TestEraWithEmptyReceipts {
968 fn blocks<M: EraMeta + ?Sized>(
969 _meta: &M,
970 _decode_receipts: bool,
971 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
972 {
973 Ok([1, 2].into_iter().map(|number| {
974 Ok((Header { number, ..Default::default() }, BlockBody::default(), Some(vec![])))
975 }))
976 }
977 }
978
979 struct TestEraWithNonZeroGenesis;
981
982 impl<R> EraBlockReader<Header, BlockBody, R> for TestEraWithNonZeroGenesis {
983 fn blocks<M: EraMeta + ?Sized>(
984 _meta: &M,
985 _decode_receipts: bool,
986 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
987 {
988 Ok([101, 102].into_iter().map(|number| {
989 Ok((Header { number, ..Default::default() }, BlockBody::default(), Some(vec![])))
990 }))
991 }
992 }
993
994 struct TestEraWithMismatchedReceipts;
997
998 impl EraBlockReader<Header, BlockBody, reth_ethereum_primitives::Receipt>
999 for TestEraWithMismatchedReceipts
1000 {
1001 fn blocks<M: EraMeta + ?Sized>(
1002 _meta: &M,
1003 _decode_receipts: bool,
1004 ) -> eyre::Result<
1005 impl Iterator<
1006 Item = eyre::Result<EraBlock<Header, BlockBody, reth_ethereum_primitives::Receipt>>,
1007 >,
1008 > {
1009 Ok(std::iter::once(Ok((
1010 Header { number: 1, ..Default::default() },
1011 BlockBody::default(),
1012 Some(vec![reth_ethereum_primitives::Receipt {
1013 tx_type: alloy_consensus::TxType::Legacy,
1014 success: true,
1015 cumulative_gas_used: 0,
1016 logs: vec![],
1017 }]),
1018 ))))
1019 }
1020 }
1021
1022 #[derive(Debug)]
1023 struct TestMeta {
1024 marked: Cell<bool>,
1025 }
1026
1027 impl EraMeta for TestMeta {
1028 fn mark_as_processed(&self) -> eyre::Result<()> {
1029 self.marked.set(true);
1030 Ok(())
1031 }
1032
1033 fn path(&self) -> &Path {
1034 Path::new("test.era1")
1035 }
1036 }
1037
1038 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1039 async fn import_stops_at_to_block() {
1040 let pf = create_test_provider_factory();
1041 init_genesis(&pf).unwrap();
1042
1043 let folder = tempdir().unwrap();
1044 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1045
1046 let stream = futures_util::stream::iter(vec![
1048 Ok(TestMeta { marked: Cell::new(false) }),
1049 Ok(TestMeta { marked: Cell::new(false) }),
1050 ]);
1051
1052 let height = import::<TestEra, _, _, _, Block, _, _>(
1053 stream,
1054 &pf,
1055 &mut hash_collector,
1056 Some(1),
1057 false,
1058 &|_| false,
1059 )
1060 .unwrap();
1061
1062 assert_eq!(height, 1);
1063 }
1064
1065 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1066 async fn backfill_does_not_move_header_checkpoints_backwards() {
1067 let pf = create_test_provider_factory();
1068 init_genesis(&pf).unwrap();
1069 let static_file_provider = pf.static_file_provider();
1070 let folder = tempdir().unwrap();
1071 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1072
1073 let provider = pf.database_provider_rw().unwrap();
1075 {
1076 let mut writer =
1077 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1078 let meta = TestMeta { marked: Cell::new(false) };
1079 process::<TestEra, _, Block, _, _>(
1080 &meta,
1081 &mut writer,
1082 None,
1083 &provider,
1084 &mut hash_collector,
1085 0..=2,
1086 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1087 )
1088 .unwrap();
1089 writer.commit().unwrap();
1090 }
1091 save_stage_checkpoints(&provider, 0, 2, 2, 2).unwrap();
1092 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(1)).unwrap();
1094 provider.commit().unwrap();
1095
1096 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1097 let height = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1098 stream,
1099 &pf,
1100 &mut hash_collector,
1101 None,
1102 true,
1103 &|_| true,
1104 )
1105 .unwrap();
1106
1107 assert_eq!(height, 1);
1108 let provider = pf.database_provider_rw().unwrap();
1109 for stage in [StageId::Headers, StageId::Bodies] {
1110 assert_eq!(
1111 provider.get_stage_checkpoint(stage).unwrap().map(|c| c.block_number),
1112 Some(2),
1113 "{stage} checkpoint must not regress below the headers already in static files"
1114 );
1115 }
1116 }
1117
1118 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1119 async fn receipt_import_requires_an_executed_range() {
1120 let pf = create_test_provider_factory();
1121 init_genesis(&pf).unwrap();
1122 let folder = tempdir().unwrap();
1123 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1124
1125 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1127 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1128 stream,
1129 &pf,
1130 &mut hash_collector,
1131 None,
1132 true,
1133 &|_| true,
1134 );
1135
1136 assert!(result.is_err());
1137 }
1138
1139 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1140 async fn receipt_import_rejects_a_to_block_off_the_execution_checkpoint() {
1141 let pf = create_test_provider_factory();
1142 init_genesis(&pf).unwrap();
1143 let folder = tempdir().unwrap();
1144 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1145
1146 let provider = pf.database_provider_rw().unwrap();
1147 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1148 provider.commit().unwrap();
1149
1150 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1152 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1153 stream,
1154 &pf,
1155 &mut hash_collector,
1156 Some(1),
1157 true,
1158 &|_| true,
1159 );
1160
1161 assert!(result.is_err());
1162 }
1163
1164 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1165 async fn receipt_import_backfills_a_completely_absent_segment() {
1166 let pf = create_test_provider_factory();
1168 let static_file_provider = pf.static_file_provider();
1169 let folder = tempdir().unwrap();
1170 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1171
1172 {
1175 let mut writer =
1176 static_file_provider.latest_writer(StaticFileSegment::Transactions).unwrap();
1177 writer.increment_block(0).unwrap();
1178 writer.commit().unwrap();
1179 }
1180
1181 let provider = pf.database_provider_rw().unwrap();
1182 {
1183 let mut writer =
1184 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1185 let genesis = Header::default();
1186 writer.append_header(&genesis, &genesis.hash_slow()).unwrap();
1187
1188 let meta = TestMeta { marked: Cell::new(false) };
1189 process::<TestEra, _, Block, _, _>(
1190 &meta,
1191 &mut writer,
1192 None,
1193 &provider,
1194 &mut hash_collector,
1195 0..=2,
1196 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1197 )
1198 .unwrap();
1199 writer.commit().unwrap();
1200 }
1201 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1202 provider.commit().unwrap();
1203
1204 assert_eq!(
1205 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1206 None
1207 );
1208
1209 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1210 let height = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1211 stream,
1212 &pf,
1213 &mut hash_collector,
1214 None,
1215 true,
1216 &|_| true,
1217 )
1218 .unwrap();
1219
1220 assert_eq!(height, 2);
1221 assert_eq!(
1222 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1223 Some(2)
1224 );
1225 }
1226
1227 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1228 async fn receipt_import_backfills_an_absent_segment_after_non_zero_genesis() {
1229 const GENESIS: u64 = 100;
1230
1231 let pf = create_test_provider_factory_with_genesis_block_number(GENESIS);
1232 let static_file_provider = pf.static_file_provider();
1233 let folder = tempdir().unwrap();
1234 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1235
1236 {
1237 let mut writer =
1238 static_file_provider.get_writer(GENESIS, StaticFileSegment::Transactions).unwrap();
1239 writer.user_header_mut().set_block_range(GENESIS, GENESIS);
1240 writer.commit().unwrap();
1241 }
1242
1243 let provider = pf.database_provider_rw().unwrap();
1244 {
1245 let mut writer =
1246 static_file_provider.get_writer(GENESIS, StaticFileSegment::Headers).unwrap();
1247 let genesis = Header { number: GENESIS, ..Default::default() };
1248 writer.user_header_mut().set_block_range(GENESIS, GENESIS);
1249 writer
1250 .append_header_direct(&genesis, genesis.difficulty, &genesis.hash_slow())
1251 .unwrap();
1252
1253 let meta = TestMeta { marked: Cell::new(false) };
1254 process::<TestEraWithNonZeroGenesis, _, Block, _, _>(
1255 &meta,
1256 &mut writer,
1257 None,
1258 &provider,
1259 &mut hash_collector,
1260 GENESIS..=102,
1261 ImportPolicy { headers_tip: GENESIS, is_receipt_verifiable: &|_| true },
1262 )
1263 .unwrap();
1264 writer.commit().unwrap();
1265 }
1266 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(102)).unwrap();
1267 provider.commit().unwrap();
1268
1269 assert_eq!(static_file_provider.genesis_block_number(), GENESIS);
1270 assert_eq!(
1271 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1272 None
1273 );
1274
1275 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1276 let height = import::<TestEraWithNonZeroGenesis, _, _, _, Block, _, _>(
1277 stream,
1278 &pf,
1279 &mut hash_collector,
1280 None,
1281 true,
1282 &|_| true,
1283 )
1284 .unwrap();
1285
1286 assert_eq!(height, 102);
1287 assert_eq!(
1288 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1289 Some(102)
1290 );
1291 }
1292
1293 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1294 async fn receipt_import_rejects_a_prune_config_that_bypasses_static_files() {
1295 let prune_modes = PruneModes {
1296 receipts_log_filter: ReceiptsLogPruneConfig(
1297 std::iter::once((Address::ZERO, PruneMode::Full)).collect(),
1298 ),
1299 ..Default::default()
1300 };
1301 let pf = create_test_provider_factory().with_prune_modes(prune_modes);
1302 init_genesis(&pf).unwrap();
1303 let folder = tempdir().unwrap();
1304 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1305
1306 let provider = pf.database_provider_rw().unwrap();
1307 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1308 provider.commit().unwrap();
1309
1310 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1311 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1312 stream,
1313 &pf,
1314 &mut hash_collector,
1315 None,
1316 true,
1317 &|_| true,
1318 );
1319
1320 assert!(result.is_err());
1321 }
1322
1323 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1324 async fn receipt_import_rejects_a_repair_starting_before_byzantium() {
1325 let pf = create_test_provider_factory();
1326 init_genesis(&pf).unwrap();
1327 let folder = tempdir().unwrap();
1328 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1329
1330 let provider = pf.database_provider_rw().unwrap();
1331 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1332 provider.commit().unwrap();
1333
1334 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1336 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1337 stream,
1338 &pf,
1339 &mut hash_collector,
1340 None,
1341 true,
1342 &|number| number >= 2,
1343 );
1344
1345 assert!(result.is_err());
1346 }
1347
1348 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1349 async fn receipt_import_errors_when_the_source_stops_short() {
1350 let pf = create_test_provider_factory();
1351 init_genesis(&pf).unwrap();
1352 let folder = tempdir().unwrap();
1353 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1354
1355 let provider = pf.database_provider_rw().unwrap();
1356 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(10)).unwrap();
1357 provider.commit().unwrap();
1358
1359 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1361 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1362 stream,
1363 &pf,
1364 &mut hash_collector,
1365 None,
1366 true,
1367 &|_| true,
1368 );
1369
1370 assert!(result.is_err());
1371 }
1372
1373 #[test]
1374 fn process_does_not_mark_partially_consumed_file_processed() {
1375 let pf = create_test_provider_factory();
1376 init_genesis(&pf).unwrap();
1377
1378 let static_file_provider = pf.static_file_provider();
1379 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1380 let provider = pf.database_provider_rw().unwrap();
1381 let folder = tempdir().unwrap();
1382 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1383 let meta = TestMeta { marked: Cell::new(false) };
1384
1385 let height = process::<TestEra, _, Block, _, _>(
1386 &meta,
1387 &mut writer,
1388 None,
1389 &provider,
1390 &mut hash_collector,
1391 0..=1,
1392 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1393 )
1394 .unwrap();
1395
1396 assert_eq!(height, 1);
1397 assert!(!meta.marked.get());
1398 }
1399
1400 #[test]
1401 fn process_iter_rejects_non_contiguous_blocks() {
1402 let pf = create_test_provider_factory();
1403 init_genesis(&pf).unwrap();
1404
1405 let static_file_provider = pf.static_file_provider();
1406 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1407 let provider = pf.database_provider_rw().unwrap();
1408 let folder = tempdir().unwrap();
1409 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1410
1411 let blocks = [5u64, 6].into_iter().map(|number| {
1414 Ok((Header { number, ..Default::default() }, BlockBody::default(), None))
1415 });
1416
1417 let result = process_iter::<_, Block, _, _>(
1418 blocks,
1419 &mut writer,
1420 None,
1421 &provider,
1422 &mut hash_collector,
1423 0..,
1424 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1425 );
1426
1427 assert!(result.is_err());
1428 }
1429
1430 #[test]
1431 fn process_writes_receipts_when_requested() {
1432 let pf = create_test_provider_factory();
1433 init_genesis(&pf).unwrap();
1434
1435 let static_file_provider = pf.static_file_provider();
1436 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1437 let mut receipts_writer =
1438 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1439 let provider = pf.database_provider_rw().unwrap();
1440 let folder = tempdir().unwrap();
1441 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1442 let meta = TestMeta { marked: Cell::new(false) };
1443
1444 let height = process::<TestEraWithEmptyReceipts, _, Block, _, _>(
1445 &meta,
1446 &mut writer,
1447 Some(&mut receipts_writer),
1448 &provider,
1449 &mut hash_collector,
1450 0..=1,
1451 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1452 )
1453 .unwrap();
1454 receipts_writer.commit().unwrap();
1455
1456 assert_eq!(height, 1);
1457 assert_eq!(
1458 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1459 Some(1)
1460 );
1461 }
1462
1463 #[test]
1464 fn process_iter_errors_when_receipts_missing() {
1465 let pf = create_test_provider_factory();
1466 init_genesis(&pf).unwrap();
1467
1468 let static_file_provider = pf.static_file_provider();
1469 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1470 let mut receipts_writer =
1471 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1472 let provider = pf.database_provider_rw().unwrap();
1473 let folder = tempdir().unwrap();
1474 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1475
1476 let blocks: Vec<
1479 eyre::Result<EraBlock<Header, BlockBody, reth_ethereum_primitives::Receipt>>,
1480 > = vec![Ok((Header { number: 1, ..Default::default() }, BlockBody::default(), None))];
1481
1482 let result = process_iter::<_, Block, _, _>(
1483 blocks.into_iter(),
1484 &mut writer,
1485 Some(&mut receipts_writer),
1486 &provider,
1487 &mut hash_collector,
1488 0..,
1489 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1490 );
1491
1492 assert!(result.is_err());
1493 }
1494
1495 #[test]
1496 fn process_errors_on_receipt_count_mismatch() {
1497 let pf = create_test_provider_factory();
1498 init_genesis(&pf).unwrap();
1499
1500 let static_file_provider = pf.static_file_provider();
1501 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1502 let mut receipts_writer =
1503 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1504 let provider = pf.database_provider_rw().unwrap();
1505 let folder = tempdir().unwrap();
1506 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1507 let meta = TestMeta { marked: Cell::new(false) };
1508
1509 let result = process::<TestEraWithMismatchedReceipts, _, Block, _, _>(
1512 &meta,
1513 &mut writer,
1514 Some(&mut receipts_writer),
1515 &provider,
1516 &mut hash_collector,
1517 0..,
1518 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1519 );
1520
1521 assert!(result.is_err());
1522 }
1523
1524 #[test]
1525 fn save_stage_checkpoints_leaves_execution_unset() {
1526 let pf = create_test_provider_factory();
1527 init_genesis(&pf).unwrap();
1528 let provider = pf.database_provider_rw().unwrap();
1529
1530 let execution_before = provider.get_stage_checkpoint(StageId::Execution).unwrap();
1531 save_stage_checkpoints(&provider, 0, 10, 10, 10).unwrap();
1532
1533 assert_eq!(
1534 provider.get_stage_checkpoint(StageId::Headers).unwrap().map(|c| c.block_number),
1535 Some(10)
1536 );
1537 assert_eq!(
1538 provider.get_stage_checkpoint(StageId::Bodies).unwrap().map(|c| c.block_number),
1539 Some(10)
1540 );
1541 assert_eq!(provider.get_stage_checkpoint(StageId::Execution).unwrap(), execution_before);
1543 }
1544
1545 #[test]
1546 fn backfills_receipts_onto_existing_headers() {
1547 let pf = create_test_provider_factory();
1548 init_genesis(&pf).unwrap();
1549 let static_file_provider = pf.static_file_provider();
1550 let folder = tempdir().unwrap();
1551 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1552
1553 {
1555 let provider = pf.database_provider_rw().unwrap();
1556 {
1559 let mut writer =
1560 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1561 let meta = TestMeta { marked: Cell::new(false) };
1562 process::<TestEra, _, Block, _, _>(
1563 &meta,
1564 &mut writer,
1565 None,
1566 &provider,
1567 &mut hash_collector,
1568 0..=2,
1569 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1570 )
1571 .unwrap();
1572 writer.commit().unwrap();
1573 }
1574 provider.commit().unwrap();
1575 }
1576 assert_eq!(
1577 static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers),
1578 Some(2)
1579 );
1580 assert_eq!(
1582 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1583 Some(0)
1584 );
1585
1586 {
1588 let provider = pf.database_provider_rw().unwrap();
1589 {
1590 let mut writer =
1591 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1592 let mut receipts_writer =
1593 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1594 let meta = TestMeta { marked: Cell::new(false) };
1595 process::<TestEraWithEmptyReceipts, _, Block, _, _>(
1596 &meta,
1597 &mut writer,
1598 Some(&mut receipts_writer),
1599 &provider,
1600 &mut hash_collector,
1601 0..,
1602 ImportPolicy { headers_tip: 2, is_receipt_verifiable: &|_| false },
1603 )
1604 .unwrap();
1605 receipts_writer.commit().unwrap();
1606 writer.commit().unwrap();
1607 }
1608 provider.commit().unwrap();
1609 }
1610
1611 assert_eq!(
1613 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1614 Some(2)
1615 );
1616 assert_eq!(
1617 static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers),
1618 Some(2)
1619 );
1620 }
1621
1622 #[test]
1623 fn process_iter_persists_verified_receipts() {
1624 let pf = create_test_provider_factory();
1625 init_genesis(&pf).unwrap();
1626 let static_file_provider = pf.static_file_provider();
1627 let folder = tempdir().unwrap();
1628 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1629
1630 let (header, body, receipt) = block_with_one_receipt(1);
1631 let provider = pf.database_provider_rw().unwrap();
1632 {
1633 let mut writer =
1634 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1635 let mut receipts_writer =
1636 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1637
1638 process_iter::<_, Block, _, _>(
1639 std::iter::once(Ok((header, body, Some(vec![receipt.clone()])))),
1640 &mut writer,
1641 Some(&mut receipts_writer),
1642 &provider,
1643 &mut hash_collector,
1644 0..,
1645 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1646 )
1647 .unwrap();
1648
1649 receipts_writer.commit().unwrap();
1650 writer.commit().unwrap();
1651 }
1652 provider.commit().unwrap();
1653
1654 let provider = pf.provider().unwrap();
1655 assert_eq!(provider.receipts_by_block(1.into()).unwrap(), Some(vec![receipt]));
1656 }
1657
1658 #[test]
1659 fn process_iter_rejects_receipts_the_header_does_not_commit_to() {
1660 let pf = create_test_provider_factory();
1661 init_genesis(&pf).unwrap();
1662 let static_file_provider = pf.static_file_provider();
1663 let folder = tempdir().unwrap();
1664 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1665
1666 let (header, body, receipt) = block_with_one_receipt(1);
1668 let tampered = Receipt { cumulative_gas_used: 42_000, ..receipt };
1669
1670 let provider = pf.database_provider_rw().unwrap();
1671 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1672 let mut receipts_writer =
1673 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1674
1675 let result = process_iter::<_, Block, _, _>(
1676 std::iter::once(Ok((header, body, Some(vec![tampered])))),
1677 &mut writer,
1678 Some(&mut receipts_writer),
1679 &provider,
1680 &mut hash_collector,
1681 0..,
1682 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1683 );
1684
1685 assert!(result.is_err());
1686 }
1687
1688 #[test]
1689 fn decodes_post_byzantium_era1_receipts() {
1690 let tuple = era1_block_tuple(4_370_000, vec![era1_receipt(Eip658Value::Eip658(true))]);
1691
1692 let (_, _, receipts) =
1693 decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true).unwrap();
1694
1695 let receipts = receipts.unwrap();
1696 assert_eq!(receipts.len(), 1);
1697 assert!(receipts[0].success);
1698 assert_eq!(receipts[0].cumulative_gas_used, 21_000);
1699 }
1700
1701 #[test]
1702 fn decodes_typed_ere_receipts_into_node_receipts() {
1703 let logs = vec![Log::new_unchecked(
1704 Address::repeat_byte(0x11),
1705 vec![B256::repeat_byte(0x22)],
1706 Bytes::from_static(b"typed"),
1707 )];
1708 let slim = vec![
1709 SlimReceipt {
1710 tx_type: TxType::Eip2930,
1711 status: Eip658Value::Eip658(true),
1712 cumulative_gas_used: 21_000,
1713 logs: logs.clone(),
1714 },
1715 SlimReceipt {
1716 tx_type: TxType::Eip1559,
1717 status: Eip658Value::Eip658(false),
1718 cumulative_gas_used: 42_000,
1719 logs: vec![],
1720 },
1721 ];
1722 let tuple = ere_block_tuple(12_965_000, &slim);
1723
1724 let (_, _, receipts) =
1725 Ere::decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true)
1726 .unwrap();
1727
1728 assert_eq!(
1729 receipts,
1730 Some(vec![
1731 Receipt {
1732 tx_type: TxType::Eip2930,
1733 success: true,
1734 cumulative_gas_used: 21_000,
1735 logs,
1736 },
1737 Receipt {
1738 tx_type: TxType::Eip1559,
1739 success: false,
1740 cumulative_gas_used: 42_000,
1741 logs: vec![],
1742 },
1743 ])
1744 );
1745 }
1746
1747 #[test]
1748 fn rejects_pre_byzantium_era1_receipts() {
1749 let tuple = era1_block_tuple(
1751 46_147,
1752 vec![era1_receipt(Eip658Value::PostState(B256::repeat_byte(1)))],
1753 );
1754
1755 let err = decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true)
1756 .unwrap_err()
1757 .to_string();
1758
1759 assert!(err.contains("pre-Byzantium"), "unexpected error: {err}");
1760 assert!(err.contains("46147"), "error should name the offending block: {err}");
1761 }
1762
1763 #[test]
1764 fn skips_pre_byzantium_receipts_when_not_requested() {
1765 let tuple = era1_block_tuple(
1767 46_147,
1768 vec![era1_receipt(Eip658Value::PostState(B256::repeat_byte(1)))],
1769 );
1770
1771 let (header, _, receipts) =
1772 decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), false).unwrap();
1773
1774 assert_eq!(header.number, 46_147);
1775 assert!(receipts.is_none());
1776 }
1777
1778 #[test]
1779 fn backfill_rejects_a_header_that_differs_from_the_persisted_one() {
1780 let pf = create_test_provider_factory();
1781 init_genesis(&pf).unwrap();
1782 let static_file_provider = pf.static_file_provider();
1783 let folder = tempdir().unwrap();
1784 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1785
1786 let provider = pf.database_provider_rw().unwrap();
1787 {
1788 let mut writer =
1789 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1790 let meta = TestMeta { marked: Cell::new(false) };
1791 process::<TestEra, _, Block, _, _>(
1792 &meta,
1793 &mut writer,
1794 None,
1795 &provider,
1796 &mut hash_collector,
1797 0..=2,
1798 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1799 )
1800 .unwrap();
1801 writer.commit().unwrap();
1802 }
1803 provider.commit().unwrap();
1804
1805 let provider = pf.database_provider_rw().unwrap();
1807 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1808 let mut receipts_writer =
1809 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1810
1811 let result = process_iter::<_, Block, _, _>(
1812 std::iter::once(Ok((
1813 Header { number: 1, gas_limit: 42, ..Default::default() },
1814 BlockBody::default(),
1815 Some(vec![]),
1816 ))),
1817 &mut writer,
1818 Some(&mut receipts_writer),
1819 &provider,
1820 &mut hash_collector,
1821 0..,
1822 ImportPolicy { headers_tip: 2, is_receipt_verifiable: &|_| false },
1823 );
1824
1825 assert!(result.is_err());
1826 }
1827
1828 #[test]
1829 fn verify_receipts_rejects_tampered_contents() {
1830 let receipts = vec![Receipt {
1831 tx_type: TxType::Legacy,
1832 success: true,
1833 cumulative_gas_used: 21_000,
1834 logs: vec![],
1835 }];
1836
1837 let with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
1839 let header = Header {
1840 receipts_root: calculate_receipt_root(&with_bloom),
1841 logs_bloom: with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref()),
1842 ..Default::default()
1843 };
1844 verify_receipts(&header, &receipts, true).unwrap();
1845
1846 let tampered = vec![Receipt { cumulative_gas_used: 42_000, ..receipts[0].clone() }];
1848 assert!(verify_receipts(&header, &tampered, true).is_err());
1849 }
1850
1851 #[test]
1852 fn verify_receipts_checks_the_bloom_even_without_the_root() {
1853 let receipts = vec![Receipt {
1854 tx_type: TxType::Legacy,
1855 success: true,
1856 cumulative_gas_used: 21_000,
1857 logs: vec![Log::new_unchecked(
1858 Address::ZERO,
1859 vec![B256::repeat_byte(1)],
1860 Bytes::default(),
1861 )],
1862 }];
1863
1864 assert!(verify_receipts(&Header::default(), &receipts, false).is_err());
1866 }
1867}