1use alloy_consensus::{
2 proofs::calculate_receipt_root, BlockHeader, ReceiptEnvelope, ReceiptWithBloom,
3 RlpDecodableReceipt, TxReceipt,
4};
5use alloy_primitives::{BlockHash, BlockNumber, Bloom, U256};
6use alloy_rlp::Decodable;
7use futures_util::{FutureExt, 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(
272 async move {
273 while let Some(file) = downloader.next().await {
274 if tx.send(Some(file)).is_err() {
275 return;
276 }
277 }
278 tx.send(None).ok();
280 }
281 .boxed(),
282 );
283
284 let static_file_provider = provider_factory.static_file_provider();
285
286 let genesis_block_number = static_file_provider.genesis_block_number();
288
289 let headers_tip = static_file_provider
292 .get_highest_static_file_block(StaticFileSegment::Headers)
293 .unwrap_or(genesis_block_number);
294
295 let receipts_tip = store_receipts
298 .then(|| static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts));
299 let mut height = match receipts_tip {
300 Some(tip) => headers_tip.min(tip.unwrap_or(genesis_block_number)),
301 None => headers_tip,
302 };
303
304 let receipts_target = store_receipts
307 .then(|| -> eyre::Result<_> {
308 let provider = provider_factory.database_provider_rw()?;
309
310 if !matches!(
313 EitherWriter::receipts_destination(&provider),
314 EitherWriterDestination::StaticFile
315 ) {
316 eyre::bail!(
317 "receipt import writes the Receipts static file segment, but this node's \
318 prune configuration keeps receipts elsewhere. Remove the receipt pruning \
319 configuration, or import without receipts"
320 );
321 }
322
323 let target = provider
324 .get_stage_checkpoint(StageId::Execution)?
325 .map(|checkpoint| checkpoint.block_number)
326 .unwrap_or(genesis_block_number);
327
328 if target <= genesis_block_number {
329 eyre::bail!(
330 "receipt import repairs the receipt static files of an already-executed \
331 range, but this database has executed no blocks. Sync the node first, or \
332 import without receipts"
333 );
334 }
335 if height >= target {
336 eyre::bail!(
337 "receipts already cover the executed range up to block {target}, nothing to \
338 repair"
339 );
340 }
341 if let Some(to_block) = to_block &&
342 to_block != target
343 {
344 eyre::bail!(
345 "--to-block {to_block} does not match the Execution checkpoint {target}. A \
346 receipt repair must cover the executed range exactly, so either drop \
347 --to-block or set it to {target}"
348 );
349 }
350 if !is_receipt_verifiable(height + 1) {
353 eyre::bail!(
354 "receipt repair would start at block {}, which predates Byzantium. Those \
355 receipts commit to a post-state root this node's receipt type cannot \
356 represent, so the Receipts segment has to already cover the pre-Byzantium \
357 range",
358 height + 1,
359 );
360 }
361
362 Ok(target)
363 })
364 .transpose()?;
365
366 if matches!(receipts_tip, Some(None)) {
371 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Receipts)?;
372 writer.user_header_mut().set_block_range(genesis_block_number, genesis_block_number);
373 writer.commit()?;
374 }
375
376 let to_block = receipts_target.or(to_block);
377 let end = to_block.map_or(Bound::Unbounded, Bound::Included);
378 let policy = ImportPolicy { headers_tip, is_receipt_verifiable };
379
380 while let Some(meta) = rx.recv()? {
381 let meta = meta?;
382 let from = height;
383 let provider = provider_factory.database_provider_rw()?;
384
385 let mut receipts_writer = store_receipts
388 .then(|| static_file_provider.latest_writer(StaticFileSegment::Receipts))
389 .transpose()?;
390
391 height = process::<S, _, _, _, _>(
392 &meta,
393 &mut static_file_provider.latest_writer(StaticFileSegment::Headers)?,
394 receipts_writer.as_mut(),
395 &provider,
396 hash_collector,
397 (Bound::Included(height), end),
398 policy,
399 )?;
400
401 drop(receipts_writer);
404
405 let checkpoint = height.max(headers_tip);
408 save_stage_checkpoints(&provider, from, checkpoint, checkpoint, checkpoint)?;
409
410 provider.commit()?;
411
412 info!(target: "era::history::import", first = from, last = height, file = %meta.path().display(), "Imported ERA file");
413
414 if to_block.is_some_and(|to| height >= to) {
415 break;
416 }
417 }
418
419 let provider = provider_factory.database_provider_rw()?;
420
421 build_index(&provider, hash_collector)?;
422
423 provider.commit()?;
424
425 if let Some(target) = receipts_target &&
428 height < target
429 {
430 eyre::bail!(
431 "receipt repair reached block {height} but the executed range ends at {target}. The \
432 source ran out of files, re-run with the files covering blocks {}..={target}",
433 height + 1,
434 );
435 }
436
437 Ok(height)
438}
439
440pub fn save_stage_checkpoints<P>(
445 provider: P,
446 from: BlockNumber,
447 to: BlockNumber,
448 processed: u64,
449 total: u64,
450) -> ProviderResult<()>
451where
452 P: StageCheckpointWriter,
453{
454 provider.save_stage_checkpoint(
455 StageId::Headers,
456 StageCheckpoint::new(to).with_headers_stage_checkpoint(HeadersCheckpoint {
457 block_range: CheckpointBlockRange { from, to },
458 progress: EntitiesCheckpoint { processed, total },
459 }),
460 )?;
461 provider.save_stage_checkpoint(
462 StageId::Bodies,
463 StageCheckpoint::new(to)
464 .with_entities_stage_checkpoint(EntitiesCheckpoint { processed, total }),
465 )?;
466 Ok(())
467}
468
469pub fn process<S, P, B, BB, BH>(
472 meta: &(impl EraMeta + ?Sized),
473 writer: &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
474 receipts_writer: Option<
475 &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
476 >,
477 provider: &P,
478 hash_collector: &mut Collector<BlockHash, BlockNumber>,
479 block_numbers: impl RangeBounds<BlockNumber>,
480 policy: ImportPolicy<'_>,
481) -> eyre::Result<BlockNumber>
482where
483 S: EraBlockReader<BH, BB, ReceiptOf<P>>,
484 B: Block<Header = BH, Body = BB>,
485 BH: FullBlockHeader + Value,
486 BB: FullBlockBody<
487 Transaction = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
488 OmmerHeader = BH,
489 >,
490 P: DBProvider<Tx: DbTxMut>
491 + NodePrimitivesProvider
492 + BlockWriter<Block = B>
493 + BlockBodyIndicesProvider
494 + BlockHashReader,
495 <P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
496 ReceiptOf<P>: Compact + Receipt,
497{
498 let decode_receipts = receipts_writer.is_some();
499 let iter = S::blocks(meta, decode_receipts)?
500 .map(Some)
501 .chain(std::iter::once_with(|| match meta.mark_as_processed() {
502 Ok(()) => None,
503 Err(error) => Some(Err(error)),
504 }))
505 .flatten();
506
507 process_iter(iter, writer, receipts_writer, provider, hash_collector, block_numbers, policy)
508}
509
510#[derive(Clone, Copy)]
512pub struct ImportPolicy<'a> {
513 pub headers_tip: BlockNumber,
515 pub is_receipt_verifiable: &'a dyn Fn(BlockNumber) -> bool,
517}
518
519impl std::fmt::Debug for ImportPolicy<'_> {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 f.debug_struct("ImportPolicy")
523 .field("headers_tip", &self.headers_tip)
524 .finish_non_exhaustive()
525 }
526}
527
528pub fn decode<BH, BB, E>(block: Result<BlockTuple, E>) -> eyre::Result<(BH, BB)>
530where
531 BH: FullBlockHeader + Value,
532 BB: FullBlockBody<OmmerHeader = BH>,
533 E: From<E2sError> + Error + Send + Sync + 'static,
534{
535 let block = block?;
536 let header: BH = block.header.decode()?;
537 let body: BB = block.body.decode()?;
538 Ok((header, body))
539}
540
541pub fn decode_with_receipts<BH, BB, R, E>(
545 block: Result<BlockTuple, E>,
546 decode_receipts: bool,
547) -> eyre::Result<EraBlock<BH, BB, R>>
548where
549 BH: FullBlockHeader + Value,
550 BB: FullBlockBody<OmmerHeader = BH>,
551 R: RlpDecodableReceipt,
552 E: From<E2sError> + Error + Send + Sync + 'static,
553{
554 let block = block?;
555 let header: BH = block.header.decode()?;
556 let body: BB = block.body.decode()?;
557 let number = header.number();
558 let receipts = decode_receipts
559 .then(|| -> eyre::Result<_> {
560 match block.receipts.decode::<Vec<ReceiptWithBloom<R>>>() {
561 Ok(receipts) => {
562 Ok(receipts.into_iter().map(|with_bloom| with_bloom.receipt).collect())
563 }
564 Err(err) => match block.receipts.decode::<Vec<ReceiptEnvelope>>() {
567 Ok(envelopes) => receipts_from_envelopes(number, envelopes),
568 Err(_) => Err(err.into()),
569 },
570 }
571 })
572 .transpose()?;
573
574 Ok((header, body, receipts))
575}
576
577fn receipts_from_envelopes<R: RlpDecodableReceipt>(
584 number: BlockNumber,
585 envelopes: Vec<ReceiptEnvelope>,
586) -> eyre::Result<Vec<R>> {
587 for envelope in &envelopes {
588 if envelope.status_or_post_state().is_post_state() {
589 eyre::bail!(
590 "block {number} has pre-Byzantium receipts, which commit to a post-state root \
591 rather than a success status and so cannot be represented by this node's receipt \
592 type. Receipt import is only supported from Byzantium onwards"
593 );
594 }
595 }
596
597 let encoded = alloy_rlp::encode(&envelopes);
598
599 Ok(Vec::<ReceiptWithBloom<R>>::decode(&mut encoded.as_slice())?
600 .into_iter()
601 .map(|with_bloom| with_bloom.receipt)
602 .collect())
603}
604
605pub fn process_iter<P, B, BB, BH>(
626 mut iter: impl Iterator<Item = eyre::Result<EraBlock<BH, BB, ReceiptOf<P>>>>,
627 writer: &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
628 mut receipts_writer: Option<
629 &mut StaticFileProviderRWRefMut<'_, <P as NodePrimitivesProvider>::Primitives>,
630 >,
631 provider: &P,
632 hash_collector: &mut Collector<BlockHash, BlockNumber>,
633 block_numbers: impl RangeBounds<BlockNumber>,
634 policy: ImportPolicy<'_>,
635) -> eyre::Result<BlockNumber>
636where
637 B: Block<Header = BH, Body = BB>,
638 BH: FullBlockHeader + Value,
639 BB: FullBlockBody<
640 Transaction = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::SignedTx,
641 OmmerHeader = BH,
642 >,
643 P: DBProvider<Tx: DbTxMut>
644 + NodePrimitivesProvider
645 + BlockWriter<Block = B>
646 + BlockBodyIndicesProvider
647 + BlockHashReader,
648 <P as NodePrimitivesProvider>::Primitives: NodePrimitives<BlockHeader = BH, BlockBody = BB>,
649 ReceiptOf<P>: Compact + Receipt,
650{
651 let mut last_header_number = match block_numbers.start_bound() {
652 Bound::Included(&number) => number,
653 Bound::Excluded(&number) => number.saturating_add(1),
654 Bound::Unbounded => 0,
655 };
656 let target = match block_numbers.end_bound() {
657 Bound::Included(&number) => Some(number),
658 Bound::Excluded(&number) => Some(number.saturating_sub(1)),
659 Bound::Unbounded => None,
660 };
661
662 for block in &mut iter {
663 let (header, body, receipts) = block?;
664 let number = header.number();
665
666 if number <= last_header_number {
667 continue;
668 }
669 if let Some(target) = target &&
670 number > target
671 {
672 break;
673 }
674
675 if number != last_header_number + 1 {
678 eyre::bail!(
679 "non-contiguous ERA import: expected block {}, got {number}; the execution \
680 database must be synced up to block {} before importing this file",
681 last_header_number + 1,
682 number - 1,
683 );
684 }
685
686 last_header_number = number;
687
688 if number > policy.headers_tip {
691 let hash = header.hash_slow();
692 writer.append_header(&header, &hash)?;
693 provider.append_block_bodies(vec![(header.number(), Some(&body))])?;
694 hash_collector.insert(hash, number)?;
695 } else if provider.block_hash(number)? != Some(header.hash_slow()) {
696 eyre::bail!(
699 "block {number} in this ERA file does not match the block already imported at \
700 that height"
701 );
702 }
703
704 if let Some(receipts_writer) = receipts_writer.as_deref_mut() {
705 if let Some(receipts) = receipts.as_deref() {
706 verify_receipts(&header, receipts, (policy.is_receipt_verifiable)(number))?;
707 }
708 provider.write_block_receipts(receipts_writer, number, receipts)?;
709 }
710 }
711
712 Ok(last_header_number)
713}
714
715fn verify_receipts<BH, R>(header: &BH, receipts: &[R], check_root: bool) -> eyre::Result<()>
720where
721 BH: FullBlockHeader,
722 R: Receipt,
723{
724 let with_bloom = receipts.iter().map(TxReceipt::with_bloom_ref).collect::<Vec<_>>();
725 let logs_bloom = with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref());
726
727 if logs_bloom != header.logs_bloom() {
728 eyre::bail!("logs bloom mismatch for block {}", header.number());
729 }
730
731 if check_root {
732 let receipts_root = calculate_receipt_root(&with_bloom);
733 if receipts_root != header.receipts_root() {
734 eyre::bail!(
735 "receipts root mismatch for block {}: computed {receipts_root}, header has {}",
736 header.number(),
737 header.receipts_root(),
738 );
739 }
740 }
741
742 Ok(())
743}
744
745trait BlockReceiptsWriterExt: BlockBodyIndicesProvider {
748 fn write_block_receipts<N: NodePrimitives>(
753 &self,
754 receipts_writer: &mut StaticFileProviderRWRefMut<'_, N>,
755 number: BlockNumber,
756 receipts: Option<Vec<N::Receipt>>,
757 ) -> eyre::Result<()>
758 where
759 N::Receipt: Compact;
760}
761
762impl<P: BlockBodyIndicesProvider> BlockReceiptsWriterExt for P {
763 fn write_block_receipts<N: NodePrimitives>(
764 &self,
765 receipts_writer: &mut StaticFileProviderRWRefMut<'_, N>,
766 number: BlockNumber,
767 receipts: Option<Vec<N::Receipt>>,
768 ) -> eyre::Result<()>
769 where
770 N::Receipt: Compact,
771 {
772 let Some(block_receipts) = receipts else {
773 eyre::bail!(
774 "block {number} has no receipts in the imported ERA file; drop --with-receipts, \
775 or import files that carry receipts for every block (`.era1` always includes \
776 them; `.ere` receipts are optional per spec)"
777 );
778 };
779
780 let indices = self
781 .block_body_indices(number)?
782 .ok_or_else(|| eyre::eyre!("missing block body indices for block {number}"))?;
783
784 if block_receipts.len() as u64 != indices.tx_count {
785 eyre::bail!(
786 "receipt count mismatch for block {number}: {} receipt(s) for {} transaction(s)",
787 block_receipts.len(),
788 indices.tx_count,
789 );
790 }
791
792 receipts_writer.increment_block(number)?;
793 receipts_writer.append_receipts(
794 (indices.first_tx_num..).zip(block_receipts.iter()).map(Ok::<_, ProviderError>),
795 )?;
796
797 Ok(())
798 }
799}
800
801pub fn build_index<P>(
803 provider: &P,
804 hash_collector: &mut Collector<BlockHash, BlockNumber>,
805) -> eyre::Result<()>
806where
807 P: DBProvider<Tx: DbTxMut>,
808{
809 let total_headers = hash_collector.len();
810 info!(target: "era::history::import", total = total_headers, "Writing headers hash index");
811
812 let mut cursor_header_numbers =
814 provider.tx_ref().cursor_write::<RawTable<tables::HeaderNumbers>>()?;
815 let first_sync = if provider.tx_ref().entries::<RawTable<tables::HeaderNumbers>>()? == 1 &&
818 let Some((hash, block_number)) = cursor_header_numbers.last()? &&
819 block_number.value()? == 0
820 {
821 hash_collector.insert(hash.key()?, 0)?;
822 cursor_header_numbers.delete_current()?;
823 true
824 } else {
825 false
826 };
827
828 let interval = (total_headers / 10).max(8192);
829
830 for (index, hash_to_number) in hash_collector.iter()?.enumerate() {
832 let (hash, number) = hash_to_number?;
833
834 if index != 0 && index.is_multiple_of(interval) {
835 info!(target: "era::history::import", progress = %format_args!("{:.2}%", (index as f64 / total_headers as f64) * 100.0), "Writing headers hash index");
836 }
837
838 let hash = RawKey::<BlockHash>::from_vec(hash);
839 let number = RawValue::<BlockNumber>::from_vec(number);
840
841 if first_sync {
842 cursor_header_numbers.append(hash, &number)?;
843 } else {
844 cursor_header_numbers.upsert(hash, &number)?;
845 }
846 }
847
848 Ok(())
849}
850
851pub fn calculate_td_by_number<P>(provider: &P, num: BlockNumber) -> eyre::Result<U256>
858where
859 P: BlockReader,
860{
861 let mut total_difficulty = U256::ZERO;
862 let mut start = 0;
863
864 while start <= num {
865 let end = (start + 1000 - 1).min(num);
866
867 total_difficulty +=
868 provider.headers_range(start..=end)?.iter().map(|h| h.difficulty()).sum::<U256>();
869
870 start = end + 1;
871 }
872
873 Ok(total_difficulty)
874}
875
876#[cfg(test)]
877mod tests {
878 use super::*;
879 use alloy_consensus::{
880 Eip658Value, Header, Receipt as RlpReceipt, ReceiptWithBloom, TxLegacy, TxType,
881 };
882 use alloy_primitives::{Address, Bytes, Log, Signature, B256};
883 use reth_db_common::init::init_genesis;
884 use reth_era::{
885 era1::types::execution::{
886 CompressedBody, CompressedHeader, CompressedReceipts, TotalDifficulty,
887 },
888 ere::types::execution::{
889 CompressedBody as EreCompressedBody, CompressedHeader as EreCompressedHeader,
890 CompressedSlimReceipts, SlimReceipt,
891 },
892 };
893 use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
894 use reth_provider::{
895 test_utils::{
896 create_test_provider_factory, create_test_provider_factory_with_genesis_block_number,
897 },
898 DatabaseProviderFactory, ReceiptProvider, StageCheckpointReader, StaticFileProviderFactory,
899 StaticFileSegment, StaticFileWriter,
900 };
901 use reth_prune_types::{PruneMode, PruneModes, ReceiptsLogPruneConfig};
902 use std::{cell::Cell, path::Path};
903 use tempfile::tempdir;
904
905 fn era1_block_tuple(number: u64, receipts: Vec<ReceiptEnvelope>) -> BlockTuple {
907 let header = Header { number, ..Default::default() };
908 BlockTuple::new(
909 CompressedHeader::from_rlp(&alloy_rlp::encode(&header)).unwrap(),
910 CompressedBody::from_rlp(&alloy_rlp::encode(BlockBody::default())).unwrap(),
911 CompressedReceipts::from_rlp(&alloy_rlp::encode(&receipts)).unwrap(),
912 TotalDifficulty::new(U256::ZERO),
913 )
914 }
915
916 fn block_with_one_receipt(number: u64) -> (Header, BlockBody, Receipt) {
918 let tx = TransactionSigned::new_unhashed(
919 TxLegacy::default().into(),
920 Signature::test_signature(),
921 );
922 let receipt = Receipt {
923 tx_type: TxType::Legacy,
924 success: true,
925 cumulative_gas_used: 21_000,
926 logs: vec![],
927 };
928
929 let with_bloom = vec![TxReceipt::with_bloom_ref(&receipt)];
930 let header = Header {
931 number,
932 receipts_root: calculate_receipt_root(&with_bloom),
933 logs_bloom: with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref()),
934 ..Default::default()
935 };
936
937 (header, BlockBody { transactions: vec![tx], ..Default::default() }, receipt)
938 }
939
940 fn era1_receipt(status: Eip658Value) -> ReceiptEnvelope {
942 ReceiptEnvelope::Legacy(ReceiptWithBloom::new(
943 RlpReceipt { status, cumulative_gas_used: 21_000, logs: vec![] },
944 Bloom::ZERO,
945 ))
946 }
947
948 fn ere_block_tuple(number: u64, receipts: &[SlimReceipt]) -> EreBlockTuple {
950 let header = Header { number, ..Default::default() };
951 EreBlockTuple::new(
952 EreCompressedHeader::from_rlp(&alloy_rlp::encode(&header)).unwrap(),
953 EreCompressedBody::from_rlp(&alloy_rlp::encode(BlockBody::default())).unwrap(),
954 )
955 .with_receipts(CompressedSlimReceipts::from_receipts(receipts).unwrap())
956 }
957
958 struct TestEra;
959
960 impl<R> EraBlockReader<Header, BlockBody, R> for TestEra {
961 fn blocks<M: EraMeta + ?Sized>(
962 _meta: &M,
963 _decode_receipts: bool,
964 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
965 {
966 Ok([1, 2].into_iter().map(|number| {
967 Ok((Header { number, ..Default::default() }, BlockBody::default(), None))
968 }))
969 }
970 }
971
972 struct TestEraWithEmptyReceipts;
974
975 impl<R> EraBlockReader<Header, BlockBody, R> for TestEraWithEmptyReceipts {
976 fn blocks<M: EraMeta + ?Sized>(
977 _meta: &M,
978 _decode_receipts: bool,
979 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
980 {
981 Ok([1, 2].into_iter().map(|number| {
982 Ok((Header { number, ..Default::default() }, BlockBody::default(), Some(vec![])))
983 }))
984 }
985 }
986
987 struct TestEraWithNonZeroGenesis;
989
990 impl<R> EraBlockReader<Header, BlockBody, R> for TestEraWithNonZeroGenesis {
991 fn blocks<M: EraMeta + ?Sized>(
992 _meta: &M,
993 _decode_receipts: bool,
994 ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
995 {
996 Ok([101, 102].into_iter().map(|number| {
997 Ok((Header { number, ..Default::default() }, BlockBody::default(), Some(vec![])))
998 }))
999 }
1000 }
1001
1002 struct TestEraWithMismatchedReceipts;
1005
1006 impl EraBlockReader<Header, BlockBody, reth_ethereum_primitives::Receipt>
1007 for TestEraWithMismatchedReceipts
1008 {
1009 fn blocks<M: EraMeta + ?Sized>(
1010 _meta: &M,
1011 _decode_receipts: bool,
1012 ) -> eyre::Result<
1013 impl Iterator<
1014 Item = eyre::Result<EraBlock<Header, BlockBody, reth_ethereum_primitives::Receipt>>,
1015 >,
1016 > {
1017 Ok(std::iter::once(Ok((
1018 Header { number: 1, ..Default::default() },
1019 BlockBody::default(),
1020 Some(vec![reth_ethereum_primitives::Receipt {
1021 tx_type: alloy_consensus::TxType::Legacy,
1022 success: true,
1023 cumulative_gas_used: 0,
1024 logs: vec![],
1025 }]),
1026 ))))
1027 }
1028 }
1029
1030 #[derive(Debug)]
1031 struct TestMeta {
1032 marked: Cell<bool>,
1033 }
1034
1035 impl EraMeta for TestMeta {
1036 fn mark_as_processed(&self) -> eyre::Result<()> {
1037 self.marked.set(true);
1038 Ok(())
1039 }
1040
1041 fn path(&self) -> &Path {
1042 Path::new("test.era1")
1043 }
1044 }
1045
1046 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1047 async fn import_stops_at_to_block() {
1048 let pf = create_test_provider_factory();
1049 init_genesis(&pf).unwrap();
1050
1051 let folder = tempdir().unwrap();
1052 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1053
1054 let stream = futures_util::stream::iter(vec![
1056 Ok(TestMeta { marked: Cell::new(false) }),
1057 Ok(TestMeta { marked: Cell::new(false) }),
1058 ]);
1059
1060 let height = import::<TestEra, _, _, _, Block, _, _>(
1061 stream,
1062 &pf,
1063 &mut hash_collector,
1064 Some(1),
1065 false,
1066 &|_| false,
1067 )
1068 .unwrap();
1069
1070 assert_eq!(height, 1);
1071 }
1072
1073 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1074 async fn backfill_does_not_move_header_checkpoints_backwards() {
1075 let pf = create_test_provider_factory();
1076 init_genesis(&pf).unwrap();
1077 let static_file_provider = pf.static_file_provider();
1078 let folder = tempdir().unwrap();
1079 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1080
1081 let provider = pf.database_provider_rw().unwrap();
1083 {
1084 let mut writer =
1085 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1086 let meta = TestMeta { marked: Cell::new(false) };
1087 process::<TestEra, _, Block, _, _>(
1088 &meta,
1089 &mut writer,
1090 None,
1091 &provider,
1092 &mut hash_collector,
1093 0..=2,
1094 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1095 )
1096 .unwrap();
1097 writer.commit().unwrap();
1098 }
1099 save_stage_checkpoints(&provider, 0, 2, 2, 2).unwrap();
1100 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(1)).unwrap();
1102 provider.commit().unwrap();
1103
1104 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1105 let height = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1106 stream,
1107 &pf,
1108 &mut hash_collector,
1109 None,
1110 true,
1111 &|_| true,
1112 )
1113 .unwrap();
1114
1115 assert_eq!(height, 1);
1116 let provider = pf.database_provider_rw().unwrap();
1117 for stage in [StageId::Headers, StageId::Bodies] {
1118 assert_eq!(
1119 provider.get_stage_checkpoint(stage).unwrap().map(|c| c.block_number),
1120 Some(2),
1121 "{stage} checkpoint must not regress below the headers already in static files"
1122 );
1123 }
1124 }
1125
1126 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1127 async fn receipt_import_requires_an_executed_range() {
1128 let pf = create_test_provider_factory();
1129 init_genesis(&pf).unwrap();
1130 let folder = tempdir().unwrap();
1131 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1132
1133 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1135 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1136 stream,
1137 &pf,
1138 &mut hash_collector,
1139 None,
1140 true,
1141 &|_| true,
1142 );
1143
1144 assert!(result.is_err());
1145 }
1146
1147 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1148 async fn receipt_import_rejects_a_to_block_off_the_execution_checkpoint() {
1149 let pf = create_test_provider_factory();
1150 init_genesis(&pf).unwrap();
1151 let folder = tempdir().unwrap();
1152 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1153
1154 let provider = pf.database_provider_rw().unwrap();
1155 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1156 provider.commit().unwrap();
1157
1158 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1160 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1161 stream,
1162 &pf,
1163 &mut hash_collector,
1164 Some(1),
1165 true,
1166 &|_| true,
1167 );
1168
1169 assert!(result.is_err());
1170 }
1171
1172 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1173 async fn receipt_import_backfills_a_completely_absent_segment() {
1174 let pf = create_test_provider_factory();
1176 let static_file_provider = pf.static_file_provider();
1177 let folder = tempdir().unwrap();
1178 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1179
1180 {
1183 let mut writer =
1184 static_file_provider.latest_writer(StaticFileSegment::Transactions).unwrap();
1185 writer.increment_block(0).unwrap();
1186 writer.commit().unwrap();
1187 }
1188
1189 let provider = pf.database_provider_rw().unwrap();
1190 {
1191 let mut writer =
1192 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1193 let genesis = Header::default();
1194 writer.append_header(&genesis, &genesis.hash_slow()).unwrap();
1195
1196 let meta = TestMeta { marked: Cell::new(false) };
1197 process::<TestEra, _, Block, _, _>(
1198 &meta,
1199 &mut writer,
1200 None,
1201 &provider,
1202 &mut hash_collector,
1203 0..=2,
1204 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1205 )
1206 .unwrap();
1207 writer.commit().unwrap();
1208 }
1209 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1210 provider.commit().unwrap();
1211
1212 assert_eq!(
1213 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1214 None
1215 );
1216
1217 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1218 let height = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1219 stream,
1220 &pf,
1221 &mut hash_collector,
1222 None,
1223 true,
1224 &|_| true,
1225 )
1226 .unwrap();
1227
1228 assert_eq!(height, 2);
1229 assert_eq!(
1230 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1231 Some(2)
1232 );
1233 }
1234
1235 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1236 async fn receipt_import_backfills_an_absent_segment_after_non_zero_genesis() {
1237 const GENESIS: u64 = 100;
1238
1239 let pf = create_test_provider_factory_with_genesis_block_number(GENESIS);
1240 let static_file_provider = pf.static_file_provider();
1241 let folder = tempdir().unwrap();
1242 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1243
1244 {
1245 let mut writer =
1246 static_file_provider.get_writer(GENESIS, StaticFileSegment::Transactions).unwrap();
1247 writer.user_header_mut().set_block_range(GENESIS, GENESIS);
1248 writer.commit().unwrap();
1249 }
1250
1251 let provider = pf.database_provider_rw().unwrap();
1252 {
1253 let mut writer =
1254 static_file_provider.get_writer(GENESIS, StaticFileSegment::Headers).unwrap();
1255 let genesis = Header { number: GENESIS, ..Default::default() };
1256 writer.user_header_mut().set_block_range(GENESIS, GENESIS);
1257 writer
1258 .append_header_direct(&genesis, genesis.difficulty, &genesis.hash_slow())
1259 .unwrap();
1260
1261 let meta = TestMeta { marked: Cell::new(false) };
1262 process::<TestEraWithNonZeroGenesis, _, Block, _, _>(
1263 &meta,
1264 &mut writer,
1265 None,
1266 &provider,
1267 &mut hash_collector,
1268 GENESIS..=102,
1269 ImportPolicy { headers_tip: GENESIS, is_receipt_verifiable: &|_| true },
1270 )
1271 .unwrap();
1272 writer.commit().unwrap();
1273 }
1274 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(102)).unwrap();
1275 provider.commit().unwrap();
1276
1277 assert_eq!(static_file_provider.genesis_block_number(), GENESIS);
1278 assert_eq!(
1279 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1280 None
1281 );
1282
1283 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1284 let height = import::<TestEraWithNonZeroGenesis, _, _, _, Block, _, _>(
1285 stream,
1286 &pf,
1287 &mut hash_collector,
1288 None,
1289 true,
1290 &|_| true,
1291 )
1292 .unwrap();
1293
1294 assert_eq!(height, 102);
1295 assert_eq!(
1296 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1297 Some(102)
1298 );
1299 }
1300
1301 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1302 async fn receipt_import_rejects_a_prune_config_that_bypasses_static_files() {
1303 let prune_modes = PruneModes {
1304 receipts_log_filter: ReceiptsLogPruneConfig(
1305 std::iter::once((Address::ZERO, PruneMode::Full)).collect(),
1306 ),
1307 ..Default::default()
1308 };
1309 let pf = create_test_provider_factory().with_prune_modes(prune_modes);
1310 init_genesis(&pf).unwrap();
1311 let folder = tempdir().unwrap();
1312 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1313
1314 let provider = pf.database_provider_rw().unwrap();
1315 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1316 provider.commit().unwrap();
1317
1318 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1319 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1320 stream,
1321 &pf,
1322 &mut hash_collector,
1323 None,
1324 true,
1325 &|_| true,
1326 );
1327
1328 assert!(result.is_err());
1329 }
1330
1331 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1332 async fn receipt_import_rejects_a_repair_starting_before_byzantium() {
1333 let pf = create_test_provider_factory();
1334 init_genesis(&pf).unwrap();
1335 let folder = tempdir().unwrap();
1336 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1337
1338 let provider = pf.database_provider_rw().unwrap();
1339 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1340 provider.commit().unwrap();
1341
1342 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1344 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1345 stream,
1346 &pf,
1347 &mut hash_collector,
1348 None,
1349 true,
1350 &|number| number >= 2,
1351 );
1352
1353 assert!(result.is_err());
1354 }
1355
1356 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1357 async fn receipt_import_errors_when_the_source_stops_short() {
1358 let pf = create_test_provider_factory();
1359 init_genesis(&pf).unwrap();
1360 let folder = tempdir().unwrap();
1361 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1362
1363 let provider = pf.database_provider_rw().unwrap();
1364 provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(10)).unwrap();
1365 provider.commit().unwrap();
1366
1367 let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1369 let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1370 stream,
1371 &pf,
1372 &mut hash_collector,
1373 None,
1374 true,
1375 &|_| true,
1376 );
1377
1378 assert!(result.is_err());
1379 }
1380
1381 #[test]
1382 fn process_does_not_mark_partially_consumed_file_processed() {
1383 let pf = create_test_provider_factory();
1384 init_genesis(&pf).unwrap();
1385
1386 let static_file_provider = pf.static_file_provider();
1387 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1388 let provider = pf.database_provider_rw().unwrap();
1389 let folder = tempdir().unwrap();
1390 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1391 let meta = TestMeta { marked: Cell::new(false) };
1392
1393 let height = process::<TestEra, _, Block, _, _>(
1394 &meta,
1395 &mut writer,
1396 None,
1397 &provider,
1398 &mut hash_collector,
1399 0..=1,
1400 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1401 )
1402 .unwrap();
1403
1404 assert_eq!(height, 1);
1405 assert!(!meta.marked.get());
1406 }
1407
1408 #[test]
1409 fn process_iter_rejects_non_contiguous_blocks() {
1410 let pf = create_test_provider_factory();
1411 init_genesis(&pf).unwrap();
1412
1413 let static_file_provider = pf.static_file_provider();
1414 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1415 let provider = pf.database_provider_rw().unwrap();
1416 let folder = tempdir().unwrap();
1417 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1418
1419 let blocks = [5u64, 6].into_iter().map(|number| {
1422 Ok((Header { number, ..Default::default() }, BlockBody::default(), None))
1423 });
1424
1425 let result = process_iter::<_, Block, _, _>(
1426 blocks,
1427 &mut writer,
1428 None,
1429 &provider,
1430 &mut hash_collector,
1431 0..,
1432 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1433 );
1434
1435 assert!(result.is_err());
1436 }
1437
1438 #[test]
1439 fn process_iter_errors_when_receipts_missing() {
1440 let pf = create_test_provider_factory();
1441 init_genesis(&pf).unwrap();
1442
1443 let static_file_provider = pf.static_file_provider();
1444 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1445 let mut receipts_writer =
1446 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1447 let provider = pf.database_provider_rw().unwrap();
1448 let folder = tempdir().unwrap();
1449 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1450
1451 let blocks: Vec<
1454 eyre::Result<EraBlock<Header, BlockBody, reth_ethereum_primitives::Receipt>>,
1455 > = vec![Ok((Header { number: 1, ..Default::default() }, BlockBody::default(), None))];
1456
1457 let result = process_iter::<_, Block, _, _>(
1458 blocks.into_iter(),
1459 &mut writer,
1460 Some(&mut receipts_writer),
1461 &provider,
1462 &mut hash_collector,
1463 0..,
1464 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1465 );
1466
1467 assert!(result.is_err());
1468 }
1469
1470 #[test]
1471 fn process_errors_on_receipt_count_mismatch() {
1472 let pf = create_test_provider_factory();
1473 init_genesis(&pf).unwrap();
1474
1475 let static_file_provider = pf.static_file_provider();
1476 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1477 let mut receipts_writer =
1478 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1479 let provider = pf.database_provider_rw().unwrap();
1480 let folder = tempdir().unwrap();
1481 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1482 let meta = TestMeta { marked: Cell::new(false) };
1483
1484 let result = process::<TestEraWithMismatchedReceipts, _, Block, _, _>(
1487 &meta,
1488 &mut writer,
1489 Some(&mut receipts_writer),
1490 &provider,
1491 &mut hash_collector,
1492 0..,
1493 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1494 );
1495
1496 assert!(result.is_err());
1497 }
1498
1499 #[test]
1500 fn save_stage_checkpoints_leaves_execution_unset() {
1501 let pf = create_test_provider_factory();
1502 init_genesis(&pf).unwrap();
1503 let provider = pf.database_provider_rw().unwrap();
1504
1505 let execution_before = provider.get_stage_checkpoint(StageId::Execution).unwrap();
1506 save_stage_checkpoints(&provider, 0, 10, 10, 10).unwrap();
1507
1508 assert_eq!(
1509 provider.get_stage_checkpoint(StageId::Headers).unwrap().map(|c| c.block_number),
1510 Some(10)
1511 );
1512 assert_eq!(
1513 provider.get_stage_checkpoint(StageId::Bodies).unwrap().map(|c| c.block_number),
1514 Some(10)
1515 );
1516 assert_eq!(provider.get_stage_checkpoint(StageId::Execution).unwrap(), execution_before);
1518 }
1519
1520 #[test]
1521 fn backfills_receipts_onto_existing_headers() {
1522 let pf = create_test_provider_factory();
1523 init_genesis(&pf).unwrap();
1524 let static_file_provider = pf.static_file_provider();
1525 let folder = tempdir().unwrap();
1526 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1527
1528 {
1530 let provider = pf.database_provider_rw().unwrap();
1531 {
1534 let mut writer =
1535 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1536 let meta = TestMeta { marked: Cell::new(false) };
1537 process::<TestEra, _, Block, _, _>(
1538 &meta,
1539 &mut writer,
1540 None,
1541 &provider,
1542 &mut hash_collector,
1543 0..=2,
1544 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1545 )
1546 .unwrap();
1547 writer.commit().unwrap();
1548 }
1549 provider.commit().unwrap();
1550 }
1551 assert_eq!(
1552 static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers),
1553 Some(2)
1554 );
1555 assert_eq!(
1557 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1558 Some(0)
1559 );
1560
1561 {
1563 let provider = pf.database_provider_rw().unwrap();
1564 {
1565 let mut writer =
1566 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1567 let mut receipts_writer =
1568 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1569 let meta = TestMeta { marked: Cell::new(false) };
1570 process::<TestEraWithEmptyReceipts, _, Block, _, _>(
1571 &meta,
1572 &mut writer,
1573 Some(&mut receipts_writer),
1574 &provider,
1575 &mut hash_collector,
1576 0..,
1577 ImportPolicy { headers_tip: 2, is_receipt_verifiable: &|_| false },
1578 )
1579 .unwrap();
1580 receipts_writer.commit().unwrap();
1581 writer.commit().unwrap();
1582 }
1583 provider.commit().unwrap();
1584 }
1585
1586 assert_eq!(
1588 static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1589 Some(2)
1590 );
1591 assert_eq!(
1592 static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers),
1593 Some(2)
1594 );
1595 }
1596
1597 #[test]
1598 fn process_iter_persists_verified_receipts() {
1599 let pf = create_test_provider_factory();
1600 init_genesis(&pf).unwrap();
1601 let static_file_provider = pf.static_file_provider();
1602 let folder = tempdir().unwrap();
1603 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1604
1605 let (header, body, receipt) = block_with_one_receipt(1);
1606 let provider = pf.database_provider_rw().unwrap();
1607 {
1608 let mut writer =
1609 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1610 let mut receipts_writer =
1611 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1612
1613 process_iter::<_, Block, _, _>(
1614 std::iter::once(Ok((header, body, Some(vec![receipt.clone()])))),
1615 &mut writer,
1616 Some(&mut receipts_writer),
1617 &provider,
1618 &mut hash_collector,
1619 0..,
1620 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1621 )
1622 .unwrap();
1623
1624 receipts_writer.commit().unwrap();
1625 writer.commit().unwrap();
1626 }
1627 provider.commit().unwrap();
1628
1629 let provider = pf.provider().unwrap();
1630 assert_eq!(provider.receipts_by_block(1.into()).unwrap(), Some(vec![receipt]));
1631 }
1632
1633 #[test]
1634 fn process_iter_rejects_receipts_the_header_does_not_commit_to() {
1635 let pf = create_test_provider_factory();
1636 init_genesis(&pf).unwrap();
1637 let static_file_provider = pf.static_file_provider();
1638 let folder = tempdir().unwrap();
1639 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1640
1641 let (header, body, receipt) = block_with_one_receipt(1);
1643 let tampered = Receipt { cumulative_gas_used: 42_000, ..receipt };
1644
1645 let provider = pf.database_provider_rw().unwrap();
1646 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1647 let mut receipts_writer =
1648 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1649
1650 let result = process_iter::<_, Block, _, _>(
1651 std::iter::once(Ok((header, body, Some(vec![tampered])))),
1652 &mut writer,
1653 Some(&mut receipts_writer),
1654 &provider,
1655 &mut hash_collector,
1656 0..,
1657 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1658 );
1659
1660 assert!(result.is_err());
1661 }
1662
1663 #[test]
1664 fn decodes_post_byzantium_era1_receipts() {
1665 let tuple = era1_block_tuple(4_370_000, vec![era1_receipt(Eip658Value::Eip658(true))]);
1666
1667 let (_, _, receipts) =
1668 decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true).unwrap();
1669
1670 let receipts = receipts.unwrap();
1671 assert_eq!(receipts.len(), 1);
1672 assert!(receipts[0].success);
1673 assert_eq!(receipts[0].cumulative_gas_used, 21_000);
1674 }
1675
1676 #[test]
1677 fn decodes_typed_ere_receipts_into_node_receipts() {
1678 let logs = vec![Log::new_unchecked(
1679 Address::repeat_byte(0x11),
1680 vec![B256::repeat_byte(0x22)],
1681 Bytes::from_static(b"typed"),
1682 )];
1683 let slim = vec![
1684 SlimReceipt {
1685 tx_type: TxType::Eip2930,
1686 status: Eip658Value::Eip658(true),
1687 cumulative_gas_used: 21_000,
1688 logs: logs.clone(),
1689 },
1690 SlimReceipt {
1691 tx_type: TxType::Eip1559,
1692 status: Eip658Value::Eip658(false),
1693 cumulative_gas_used: 42_000,
1694 logs: vec![],
1695 },
1696 ];
1697 let tuple = ere_block_tuple(12_965_000, &slim);
1698
1699 let (_, _, receipts) =
1700 Ere::decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true)
1701 .unwrap();
1702
1703 assert_eq!(
1704 receipts,
1705 Some(vec![
1706 Receipt {
1707 tx_type: TxType::Eip2930,
1708 success: true,
1709 cumulative_gas_used: 21_000,
1710 logs,
1711 },
1712 Receipt {
1713 tx_type: TxType::Eip1559,
1714 success: false,
1715 cumulative_gas_used: 42_000,
1716 logs: vec![],
1717 },
1718 ])
1719 );
1720 }
1721
1722 #[test]
1723 fn rejects_pre_byzantium_era1_receipts() {
1724 let tuple = era1_block_tuple(
1726 46_147,
1727 vec![era1_receipt(Eip658Value::PostState(B256::repeat_byte(1)))],
1728 );
1729
1730 let err = decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true)
1731 .unwrap_err()
1732 .to_string();
1733
1734 assert!(err.contains("pre-Byzantium"), "unexpected error: {err}");
1735 assert!(err.contains("46147"), "error should name the offending block: {err}");
1736 }
1737
1738 #[test]
1739 fn skips_pre_byzantium_receipts_when_not_requested() {
1740 let tuple = era1_block_tuple(
1742 46_147,
1743 vec![era1_receipt(Eip658Value::PostState(B256::repeat_byte(1)))],
1744 );
1745
1746 let (header, _, receipts) =
1747 decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), false).unwrap();
1748
1749 assert_eq!(header.number, 46_147);
1750 assert!(receipts.is_none());
1751 }
1752
1753 #[test]
1754 fn backfill_rejects_a_header_that_differs_from_the_persisted_one() {
1755 let pf = create_test_provider_factory();
1756 init_genesis(&pf).unwrap();
1757 let static_file_provider = pf.static_file_provider();
1758 let folder = tempdir().unwrap();
1759 let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1760
1761 let provider = pf.database_provider_rw().unwrap();
1762 {
1763 let mut writer =
1764 static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1765 let meta = TestMeta { marked: Cell::new(false) };
1766 process::<TestEra, _, Block, _, _>(
1767 &meta,
1768 &mut writer,
1769 None,
1770 &provider,
1771 &mut hash_collector,
1772 0..=2,
1773 ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1774 )
1775 .unwrap();
1776 writer.commit().unwrap();
1777 }
1778 provider.commit().unwrap();
1779
1780 let provider = pf.database_provider_rw().unwrap();
1782 let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1783 let mut receipts_writer =
1784 static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1785
1786 let result = process_iter::<_, Block, _, _>(
1787 std::iter::once(Ok((
1788 Header { number: 1, gas_limit: 42, ..Default::default() },
1789 BlockBody::default(),
1790 Some(vec![]),
1791 ))),
1792 &mut writer,
1793 Some(&mut receipts_writer),
1794 &provider,
1795 &mut hash_collector,
1796 0..,
1797 ImportPolicy { headers_tip: 2, is_receipt_verifiable: &|_| false },
1798 );
1799
1800 assert!(result.is_err());
1801 }
1802
1803 #[test]
1804 fn verify_receipts_checks_the_bloom_even_without_the_root() {
1805 let receipts = vec![Receipt {
1806 tx_type: TxType::Legacy,
1807 success: true,
1808 cumulative_gas_used: 21_000,
1809 logs: vec![Log::new_unchecked(
1810 Address::ZERO,
1811 vec![B256::repeat_byte(1)],
1812 Bytes::default(),
1813 )],
1814 }];
1815
1816 assert!(verify_receipts(&Header::default(), &receipts, false).is_err());
1818 }
1819}