Skip to main content

reth_era_utils/
history.rs

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
44/// A decoded ERA block: header, body, and optionally its receipts.
45type EraBlock<BH, BB, R> = (BH, BB, Option<Vec<R>>);
46
47/// The receipt type of the node primitives behind provider `P`.
48type ReceiptOf<P> = <<P as NodePrimitivesProvider>::Primitives as NodePrimitives>::Receipt;
49
50/// Reads execution `(header, body, receipts)` tuples out of an ERA file.
51///
52/// `receipts` is `None` when `decode_receipts` is `false`, or the file has none (`.era` never
53/// does; `.ere` receipts are optional). `decode_receipts = false` skips decoding entirely.
54///
55/// Per-format seam of the import pipeline.
56pub trait EraBlockReader<BH, BB, R> {
57    /// Opens the ERA file at `meta` and iterates its execution blocks.
58    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/// [`EraBlockReader`] for `.era1` files.
65#[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/// [`EraBlockReader`] for `.ere`/`.erae` files.
101#[derive(Debug)]
102pub struct Ere;
103
104impl Ere {
105    /// Extracts a `(header, body)` pair from an ERE block tuple, whose header and body are
106    /// RLP-compressed. Ignores any receipts entry; callers that need receipts should use
107    /// [`decode_with_receipts`](Self::decode_with_receipts).
108    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    /// Like [`decode`](Self::decode), but also extracts receipts when `decode_receipts` is `true`.
121    /// `receipts` is `None` if `decode_receipts` is `false`, or if the block tuple carries no
122    /// receipts entry (`.ere` receipts are optional per spec).
123    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/// [`EraBlockReader`] for consensus-layer `.era` files.
149///
150/// `.era` files store consensus `SignedBeaconBlock`s. Post-merge
151/// blocks embed an execution payload; this source SSZ-decodes each beacon block, extracts that
152/// payload, and converts it into an execution `(header, body)` pair. Pre-merge slots carry no
153/// payload and are skipped. `.era` files carry no receipt data at all, so this source always
154/// yields `None` for receipts.
155#[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    /// `.era` files carry no receipt data, so `decode_receipts` has no effect.
164    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    /// Decodes the execution `(header, body)` embedded in a consensus `SignedBeaconBlock`.
180    ///
181    /// Returns `Ok(None)` for pre-merge slots, which carry no execution payload.
182    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        // The beacon payload decodes into alloy execution types; re-encode and decode into the
196        // node's own primitives through the same RLP representation the `.era1`/`.ere` paths use.
197        Ok(Some((reencode_rlp(&header, buf)?, reencode_rlp(&body, buf)?)))
198    }
199}
200
201/// Re-encodes an alloy execution type as RLP and decodes it back into the node's primitive type.
202///
203/// `.era` files yield alloy execution headers/bodies, while the import pipeline writes the node's
204/// own primitives. Both share the canonical execution RLP encoding, so a round-trip bridges them
205/// without requiring a direct `From` conversion.
206fn 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
216/// Opens the ERA file at `meta` with the format's [`StreamReader`].
217pub 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
224/// Imports blocks from `downloader`, decoding each file with the [`EraBlockReader`] `S`.
225///
226/// When `to_block` is set, the import stops after reaching that block height; otherwise it
227/// continues until the source has no more files.
228///
229/// `store_receipts` backfills the `Receipts` segment from its tip up to the `Execution`
230/// checkpoint, which it must reach exactly: receipts above the checkpoint are pruned on the next
231/// node start, and receipts below it leave the node unable to start. Only a missing tail is
232/// detected, never a gap inside the existing segment.
233///
234/// `is_receipt_verifiable` decides which blocks have their receipts checked against the header,
235/// as pre-Byzantium receipts can't be recomputed here.
236///
237/// Returns current block height.
238pub 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    // Handle IO-bound async download in a background tokio task
271    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    // The chain's first block, which is not necessarily 0: reth supports a non-zero genesis.
281    let genesis_block_number = static_file_provider.genesis_block_number();
282
283    // Consistency check of expected headers in static files vs DB is done on provider::sync_gap
284    // when poll_execute_ready is polled.
285    let headers_tip = static_file_provider
286        .get_highest_static_file_block(StaticFileSegment::Headers)
287        .unwrap_or(genesis_block_number);
288
289    // When backfilling receipts, resume from the receipts tip so blocks whose headers were already
290    // imported still get their receipts; only blocks above `headers_tip` are written in full.
291    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    // Only the executed range can hold receipts durably, so the repair has to land exactly on the
299    // Execution checkpoint.
300    let receipts_target = store_receipts
301        .then(|| -> eyre::Result<_> {
302            let provider = provider_factory.database_provider_rw()?;
303
304            // Writing the segment under a config that routes receipts to the database would leave
305            // two sources and keep data the prune config wants dropped.
306            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            // Such a repair can never complete, so fail before committing the empty blocks that
345            // precede the first pre-Byzantium receipt.
346            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    // A segment that doesn't exist yet holds no receipts for genesis, which has none of its own.
361    // The backfill resumes at the block after it, so seed that entry the way `init_genesis` does,
362    // or the writer rejects the first append. Setting the range directly rather than incrementing
363    // also covers a genesis that doesn't sit on a static file boundary.
364    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        // Headers and receipts are separate static file segments, each with its own writer, only
380        // open the receipts one when `--with-receipts` was requested.
381        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 the receipts writer's lock before `provider.commit()`, which locks every static
396        // file segment (including receipts) via `has_unwind_queued`.
397        drop(receipts_writer);
398
399        // A receipts-only backfill trails `headers_tip`; the checkpoints must not regress below
400        // the headers already in static files.
401        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    // A repair that stops short leaves the receipts behind the executed range, which the Execution
420    // stage reports as missing static file data on the next node start.
421    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
434/// Saves progress of ERA import into stages sync.
435///
436/// Never marks `Execution` done: this import writes no state, so moving that checkpoint would let
437/// a node start from blocks whose accounts and storage were never written.
438pub 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
463/// Reads `meta` with the [`EraBlockReader`] `S`, appends its blocks within `block_numbers`, and
464/// marks `meta` processed if the file was fully consumed. Returns last block height.
465pub 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/// Per-block import policy for [`process`] and [`process_iter`].
505#[derive(Clone, Copy)]
506pub struct ImportPolicy<'a> {
507    /// Blocks at or below this are backfilled receipts-only; blocks above it are written in full.
508    pub headers_tip: BlockNumber,
509    /// Whether a block's receipts are verified against its header commitments.
510    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        // `is_receipt_verifiable` is a `dyn Fn` and can't be formatted.
516        f.debug_struct("ImportPolicy")
517            .field("headers_tip", &self.headers_tip)
518            .finish_non_exhaustive()
519    }
520}
521
522/// Extracts a `(header, body)` pair from [`BlockTuple`].
523pub 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
535/// Like [`decode`], but also extracts receipts when `decode_receipts` is `true`. `receipts` is
536/// `Some` whenever `decode_receipts` is `true`: `era1`'s `CompressedReceipts` entry is mandatory
537/// per spec, so (unlike `.ere`) it is never `None` just because the file omits it.
538pub 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                // A status field holding a post-state root doesn't fit the node's receipt type, so
559                // re-read the entry to tell that apart from a genuinely malformed one.
560                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
571/// Converts decoded ERA receipts into the node's receipt type.
572///
573/// Rebuilds them through their canonical encoding, the only construction path every node receipt
574/// type supports. Receipts predating Byzantium are rejected: they commit to a post-state root
575/// rather than a success status, and nothing else in the file records whether the transaction
576/// succeeded, so no node receipt type can represent them.
577fn 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
599/// Extracts block headers, bodies and (optionally) receipts from `iter` and appends them using
600/// `writer`, `receipts_writer` and `provider`.
601///
602/// Collects hash to height using `hash_collector`.
603///
604/// Skips all blocks below the [`start_bound`] of `block_numbers` and stops when reaching past the
605/// [`end_bound`] or the end of the file.
606///
607/// Blocks at or below `headers_tip` only have their receipts backfilled; blocks above it get
608/// header, body and receipts written.
609///
610/// When `receipts_writer` is `Some`, every block must carry receipts, a block without them,
611/// possible for `.ere` files, whose receipts are optional per spec is an error. Receipts are
612/// checked against the header's logs bloom, and against its receipts root when
613/// `is_receipt_verifiable` returns `true`.
614///
615/// Returns last block height.
616///
617/// [`start_bound`]: RangeBounds::start_bound
618/// [`end_bound`]: RangeBounds::end_bound
619pub 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        // Reject gaps: the import marks the Headers/Bodies stages complete up to `height`, so a
670        // non-contiguous append would leave earlier blocks missing while the stages report done.
671        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        // Header and body are only written for new blocks, when backfilling receipts onto an
683        // earlier import the block already has both persisted.
684        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            // Receipts are verified against the source header, so it must be the block already
691            // persisted at this height, not merely a self-consistent one.
692            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
709/// Checks a block's receipts against its header.
710///
711/// The bloom is always checked, the root only when `check_root` is set, as pre-Byzantium receipts
712/// can't be recomputed here.
713fn 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
739/// Lets a provider append one block's receipts to a `Receipts` static file writer, using itself
740/// to look up the block's transaction range.
741trait BlockReceiptsWriterExt: BlockBodyIndicesProvider {
742    /// Appends `receipts` for block `number` to `receipts_writer`.
743    ///
744    /// Errors if the block has no receipts, or if the receipt count doesn't match the block's
745    /// transaction count.
746    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
795/// Dumps the contents of `hash_collector` into [`tables::HeaderNumbers`].
796pub 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    // Database cursor for hash to number index
807    let mut cursor_header_numbers =
808        provider.tx_ref().cursor_write::<RawTable<tables::HeaderNumbers>>()?;
809    // If we only have the genesis block hash, then we are at first sync, and we can remove it,
810    // add it to the collector and use tx.append on all hashes.
811    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    // Build block hash to block number index
825    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
845/// Calculates the total difficulty for a given block number by summing the difficulty
846/// of all blocks from genesis to the given block.
847///
848/// Very expensive - iterates through all blocks in batches of 1000.
849///
850/// Returns an error if any block is missing.
851pub 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    /// Builds an `.era1` block tuple for a transaction-less block carrying `receipts`.
898    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    /// Builds a block with one transaction, its receipt, and a header committing to that receipt.
909    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    /// Wraps `status` in a legacy `.era1` receipt envelope.
933    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    /// Builds an `.ere` block tuple carrying canonical slim receipts.
941    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    /// Like [`TestEra`], but yields `Some(vec![])` receipts for each (transaction-less) block.
965    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    /// Empty receipts for the two blocks immediately after a genesis at block 100.
980    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    /// Like [`TestEra`], but yields a single receipt for a transaction-less block, provoking a
995    /// receipt/transaction count mismatch.
996    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        // Each file yields blocks 1 and 2; without `to_block` the import would reach 2.
1047        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        // Import headers and bodies for blocks 1 and 2 first.
1074        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        // Only block 1 was executed, so the repair ends below the headers already in static files.
1093        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        // Nothing executed, so there is no range whose receipts could survive a node start.
1126        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        // Stopping at 1 would leave the receipts behind the executed range.
1151        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        // No genesis, so the Receipts segment does not exist at all.
1167        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        // Stand in for the genesis entries `init_genesis` would write to every segment, leaving
1173        // only `Receipts` absent.
1174        {
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        // Receipts resume at block 1, before this chain's Byzantium activation at block 2.
1335        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        // The source only carries blocks 1 and 2, far short of the executed range.
1360        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        // Genesis DB sits at height 0, but the first block is 5: a gap that must be rejected
1412        // rather than appended (as a pre-merge `.era` import would otherwise produce).
1413        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        // `TestEra` never yields receipts; requesting them must fail rather than silently import
1477        // headers/bodies without them.
1478        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        // One receipt for a transaction-less body: a count mismatch that must be rejected rather
1510        // than silently misaligning the Receipts static file against Transactions.
1511        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        // Receipt import doesn't produce state, so it must not advance Execution.
1542        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        // First pass: import headers and bodies only, no receipts.
1554        {
1555            let provider = pf.database_provider_rw().unwrap();
1556            // The writer holds the Headers segment lock, which `provider.commit()` also takes when
1557            // it finalizes every segment, so it must be released first.
1558            {
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        // Receipts still only cover genesis; blocks 1 and 2 have headers but no receipts yet.
1581        assert_eq!(
1582            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1583            Some(0)
1584        );
1585
1586        // Second pass: backfill receipts for the already-imported headers (`headers_tip = 2`).
1587        {
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        // Receipts now cover the previously header-only range, and headers weren't re-appended.
1612        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        // Right receipt count, wrong contents: only the recomputed root catches this.
1667        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        // Mainnet's first transaction: its receipt commits to a post-state root.
1750        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        // A header-only import of the same file must stay unaffected.
1766        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        // Same height and receipt count as the persisted block, but a different header.
1806        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        // Commit the header to the receipts as decoded.
1838        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        // Same receipt count, different contents: the recomputed root no longer matches.
1847        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        // The header commits to no logs at all, so the bloom cannot match.
1865        assert!(verify_receipts(&Header::default(), &receipts, false).is_err());
1866    }
1867}