Skip to main content

reth_era_utils/
history.rs

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::{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 envelope.status_or_post_state().is_post_state() {
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_args!("{:.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::{
874        Eip658Value, Header, Receipt as RlpReceipt, ReceiptWithBloom, TxLegacy, TxType,
875    };
876    use alloy_primitives::{Address, Bytes, Log, Signature, B256};
877    use reth_db_common::init::init_genesis;
878    use reth_era::{
879        era1::types::execution::{
880            CompressedBody, CompressedHeader, CompressedReceipts, TotalDifficulty,
881        },
882        ere::types::execution::{
883            CompressedBody as EreCompressedBody, CompressedHeader as EreCompressedHeader,
884            CompressedSlimReceipts, SlimReceipt,
885        },
886    };
887    use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
888    use reth_provider::{
889        test_utils::{
890            create_test_provider_factory, create_test_provider_factory_with_genesis_block_number,
891        },
892        DatabaseProviderFactory, ReceiptProvider, StageCheckpointReader, StaticFileProviderFactory,
893        StaticFileSegment, StaticFileWriter,
894    };
895    use reth_prune_types::{PruneMode, PruneModes, ReceiptsLogPruneConfig};
896    use std::{cell::Cell, path::Path};
897    use tempfile::tempdir;
898
899    /// Builds an `.era1` block tuple for a transaction-less block carrying `receipts`.
900    fn era1_block_tuple(number: u64, receipts: Vec<ReceiptEnvelope>) -> BlockTuple {
901        let header = Header { number, ..Default::default() };
902        BlockTuple::new(
903            CompressedHeader::from_rlp(&alloy_rlp::encode(&header)).unwrap(),
904            CompressedBody::from_rlp(&alloy_rlp::encode(BlockBody::default())).unwrap(),
905            CompressedReceipts::from_rlp(&alloy_rlp::encode(&receipts)).unwrap(),
906            TotalDifficulty::new(U256::ZERO),
907        )
908    }
909
910    /// Builds a block with one transaction, its receipt, and a header committing to that receipt.
911    fn block_with_one_receipt(number: u64) -> (Header, BlockBody, Receipt) {
912        let tx = TransactionSigned::new_unhashed(
913            TxLegacy::default().into(),
914            Signature::test_signature(),
915        );
916        let receipt = Receipt {
917            tx_type: TxType::Legacy,
918            success: true,
919            cumulative_gas_used: 21_000,
920            logs: vec![],
921        };
922
923        let with_bloom = vec![TxReceipt::with_bloom_ref(&receipt)];
924        let header = Header {
925            number,
926            receipts_root: calculate_receipt_root(&with_bloom),
927            logs_bloom: with_bloom.iter().fold(Bloom::ZERO, |bloom, r| bloom | r.bloom_ref()),
928            ..Default::default()
929        };
930
931        (header, BlockBody { transactions: vec![tx], ..Default::default() }, receipt)
932    }
933
934    /// Wraps `status` in a legacy `.era1` receipt envelope.
935    fn era1_receipt(status: Eip658Value) -> ReceiptEnvelope {
936        ReceiptEnvelope::Legacy(ReceiptWithBloom::new(
937            RlpReceipt { status, cumulative_gas_used: 21_000, logs: vec![] },
938            Bloom::ZERO,
939        ))
940    }
941
942    /// Builds an `.ere` block tuple carrying canonical slim receipts.
943    fn ere_block_tuple(number: u64, receipts: &[SlimReceipt]) -> EreBlockTuple {
944        let header = Header { number, ..Default::default() };
945        EreBlockTuple::new(
946            EreCompressedHeader::from_rlp(&alloy_rlp::encode(&header)).unwrap(),
947            EreCompressedBody::from_rlp(&alloy_rlp::encode(BlockBody::default())).unwrap(),
948        )
949        .with_receipts(CompressedSlimReceipts::from_receipts(receipts).unwrap())
950    }
951
952    struct TestEra;
953
954    impl<R> EraBlockReader<Header, BlockBody, R> for TestEra {
955        fn blocks<M: EraMeta + ?Sized>(
956            _meta: &M,
957            _decode_receipts: bool,
958        ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
959        {
960            Ok([1, 2].into_iter().map(|number| {
961                Ok((Header { number, ..Default::default() }, BlockBody::default(), None))
962            }))
963        }
964    }
965
966    /// Like [`TestEra`], but yields `Some(vec![])` receipts for each (transaction-less) block.
967    struct TestEraWithEmptyReceipts;
968
969    impl<R> EraBlockReader<Header, BlockBody, R> for TestEraWithEmptyReceipts {
970        fn blocks<M: EraMeta + ?Sized>(
971            _meta: &M,
972            _decode_receipts: bool,
973        ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
974        {
975            Ok([1, 2].into_iter().map(|number| {
976                Ok((Header { number, ..Default::default() }, BlockBody::default(), Some(vec![])))
977            }))
978        }
979    }
980
981    /// Empty receipts for the two blocks immediately after a genesis at block 100.
982    struct TestEraWithNonZeroGenesis;
983
984    impl<R> EraBlockReader<Header, BlockBody, R> for TestEraWithNonZeroGenesis {
985        fn blocks<M: EraMeta + ?Sized>(
986            _meta: &M,
987            _decode_receipts: bool,
988        ) -> eyre::Result<impl Iterator<Item = eyre::Result<(Header, BlockBody, Option<Vec<R>>)>>>
989        {
990            Ok([101, 102].into_iter().map(|number| {
991                Ok((Header { number, ..Default::default() }, BlockBody::default(), Some(vec![])))
992            }))
993        }
994    }
995
996    /// Like [`TestEra`], but yields a single receipt for a transaction-less block, provoking a
997    /// receipt/transaction count mismatch.
998    struct TestEraWithMismatchedReceipts;
999
1000    impl EraBlockReader<Header, BlockBody, reth_ethereum_primitives::Receipt>
1001        for TestEraWithMismatchedReceipts
1002    {
1003        fn blocks<M: EraMeta + ?Sized>(
1004            _meta: &M,
1005            _decode_receipts: bool,
1006        ) -> eyre::Result<
1007            impl Iterator<
1008                Item = eyre::Result<EraBlock<Header, BlockBody, reth_ethereum_primitives::Receipt>>,
1009            >,
1010        > {
1011            Ok(std::iter::once(Ok((
1012                Header { number: 1, ..Default::default() },
1013                BlockBody::default(),
1014                Some(vec![reth_ethereum_primitives::Receipt {
1015                    tx_type: alloy_consensus::TxType::Legacy,
1016                    success: true,
1017                    cumulative_gas_used: 0,
1018                    logs: vec![],
1019                }]),
1020            ))))
1021        }
1022    }
1023
1024    #[derive(Debug)]
1025    struct TestMeta {
1026        marked: Cell<bool>,
1027    }
1028
1029    impl EraMeta for TestMeta {
1030        fn mark_as_processed(&self) -> eyre::Result<()> {
1031            self.marked.set(true);
1032            Ok(())
1033        }
1034
1035        fn path(&self) -> &Path {
1036            Path::new("test.era1")
1037        }
1038    }
1039
1040    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1041    async fn import_stops_at_to_block() {
1042        let pf = create_test_provider_factory();
1043        init_genesis(&pf).unwrap();
1044
1045        let folder = tempdir().unwrap();
1046        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1047
1048        // Each file yields blocks 1 and 2; without `to_block` the import would reach 2.
1049        let stream = futures_util::stream::iter(vec![
1050            Ok(TestMeta { marked: Cell::new(false) }),
1051            Ok(TestMeta { marked: Cell::new(false) }),
1052        ]);
1053
1054        let height = import::<TestEra, _, _, _, Block, _, _>(
1055            stream,
1056            &pf,
1057            &mut hash_collector,
1058            Some(1),
1059            false,
1060            &|_| false,
1061        )
1062        .unwrap();
1063
1064        assert_eq!(height, 1);
1065    }
1066
1067    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1068    async fn backfill_does_not_move_header_checkpoints_backwards() {
1069        let pf = create_test_provider_factory();
1070        init_genesis(&pf).unwrap();
1071        let static_file_provider = pf.static_file_provider();
1072        let folder = tempdir().unwrap();
1073        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1074
1075        // Import headers and bodies for blocks 1 and 2 first.
1076        let provider = pf.database_provider_rw().unwrap();
1077        {
1078            let mut writer =
1079                static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1080            let meta = TestMeta { marked: Cell::new(false) };
1081            process::<TestEra, _, Block, _, _>(
1082                &meta,
1083                &mut writer,
1084                None,
1085                &provider,
1086                &mut hash_collector,
1087                0..=2,
1088                ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1089            )
1090            .unwrap();
1091            writer.commit().unwrap();
1092        }
1093        save_stage_checkpoints(&provider, 0, 2, 2, 2).unwrap();
1094        // Only block 1 was executed, so the repair ends below the headers already in static files.
1095        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(1)).unwrap();
1096        provider.commit().unwrap();
1097
1098        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1099        let height = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1100            stream,
1101            &pf,
1102            &mut hash_collector,
1103            None,
1104            true,
1105            &|_| true,
1106        )
1107        .unwrap();
1108
1109        assert_eq!(height, 1);
1110        let provider = pf.database_provider_rw().unwrap();
1111        for stage in [StageId::Headers, StageId::Bodies] {
1112            assert_eq!(
1113                provider.get_stage_checkpoint(stage).unwrap().map(|c| c.block_number),
1114                Some(2),
1115                "{stage} checkpoint must not regress below the headers already in static files"
1116            );
1117        }
1118    }
1119
1120    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1121    async fn receipt_import_requires_an_executed_range() {
1122        let pf = create_test_provider_factory();
1123        init_genesis(&pf).unwrap();
1124        let folder = tempdir().unwrap();
1125        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1126
1127        // Nothing executed, so there is no range whose receipts could survive a node start.
1128        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1129        let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1130            stream,
1131            &pf,
1132            &mut hash_collector,
1133            None,
1134            true,
1135            &|_| true,
1136        );
1137
1138        assert!(result.is_err());
1139    }
1140
1141    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1142    async fn receipt_import_rejects_a_to_block_off_the_execution_checkpoint() {
1143        let pf = create_test_provider_factory();
1144        init_genesis(&pf).unwrap();
1145        let folder = tempdir().unwrap();
1146        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1147
1148        let provider = pf.database_provider_rw().unwrap();
1149        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1150        provider.commit().unwrap();
1151
1152        // Stopping at 1 would leave the receipts behind the executed range.
1153        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1154        let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1155            stream,
1156            &pf,
1157            &mut hash_collector,
1158            Some(1),
1159            true,
1160            &|_| true,
1161        );
1162
1163        assert!(result.is_err());
1164    }
1165
1166    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1167    async fn receipt_import_backfills_a_completely_absent_segment() {
1168        // No genesis, so the Receipts segment does not exist at all.
1169        let pf = create_test_provider_factory();
1170        let static_file_provider = pf.static_file_provider();
1171        let folder = tempdir().unwrap();
1172        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1173
1174        // Stand in for the genesis entries `init_genesis` would write to every segment, leaving
1175        // only `Receipts` absent.
1176        {
1177            let mut writer =
1178                static_file_provider.latest_writer(StaticFileSegment::Transactions).unwrap();
1179            writer.increment_block(0).unwrap();
1180            writer.commit().unwrap();
1181        }
1182
1183        let provider = pf.database_provider_rw().unwrap();
1184        {
1185            let mut writer =
1186                static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1187            let genesis = Header::default();
1188            writer.append_header(&genesis, &genesis.hash_slow()).unwrap();
1189
1190            let meta = TestMeta { marked: Cell::new(false) };
1191            process::<TestEra, _, Block, _, _>(
1192                &meta,
1193                &mut writer,
1194                None,
1195                &provider,
1196                &mut hash_collector,
1197                0..=2,
1198                ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1199            )
1200            .unwrap();
1201            writer.commit().unwrap();
1202        }
1203        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1204        provider.commit().unwrap();
1205
1206        assert_eq!(
1207            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1208            None
1209        );
1210
1211        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1212        let height = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1213            stream,
1214            &pf,
1215            &mut hash_collector,
1216            None,
1217            true,
1218            &|_| true,
1219        )
1220        .unwrap();
1221
1222        assert_eq!(height, 2);
1223        assert_eq!(
1224            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1225            Some(2)
1226        );
1227    }
1228
1229    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1230    async fn receipt_import_backfills_an_absent_segment_after_non_zero_genesis() {
1231        const GENESIS: u64 = 100;
1232
1233        let pf = create_test_provider_factory_with_genesis_block_number(GENESIS);
1234        let static_file_provider = pf.static_file_provider();
1235        let folder = tempdir().unwrap();
1236        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1237
1238        {
1239            let mut writer =
1240                static_file_provider.get_writer(GENESIS, StaticFileSegment::Transactions).unwrap();
1241            writer.user_header_mut().set_block_range(GENESIS, GENESIS);
1242            writer.commit().unwrap();
1243        }
1244
1245        let provider = pf.database_provider_rw().unwrap();
1246        {
1247            let mut writer =
1248                static_file_provider.get_writer(GENESIS, StaticFileSegment::Headers).unwrap();
1249            let genesis = Header { number: GENESIS, ..Default::default() };
1250            writer.user_header_mut().set_block_range(GENESIS, GENESIS);
1251            writer
1252                .append_header_direct(&genesis, genesis.difficulty, &genesis.hash_slow())
1253                .unwrap();
1254
1255            let meta = TestMeta { marked: Cell::new(false) };
1256            process::<TestEraWithNonZeroGenesis, _, Block, _, _>(
1257                &meta,
1258                &mut writer,
1259                None,
1260                &provider,
1261                &mut hash_collector,
1262                GENESIS..=102,
1263                ImportPolicy { headers_tip: GENESIS, is_receipt_verifiable: &|_| true },
1264            )
1265            .unwrap();
1266            writer.commit().unwrap();
1267        }
1268        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(102)).unwrap();
1269        provider.commit().unwrap();
1270
1271        assert_eq!(static_file_provider.genesis_block_number(), GENESIS);
1272        assert_eq!(
1273            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1274            None
1275        );
1276
1277        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1278        let height = import::<TestEraWithNonZeroGenesis, _, _, _, Block, _, _>(
1279            stream,
1280            &pf,
1281            &mut hash_collector,
1282            None,
1283            true,
1284            &|_| true,
1285        )
1286        .unwrap();
1287
1288        assert_eq!(height, 102);
1289        assert_eq!(
1290            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1291            Some(102)
1292        );
1293    }
1294
1295    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1296    async fn receipt_import_rejects_a_prune_config_that_bypasses_static_files() {
1297        let prune_modes = PruneModes {
1298            receipts_log_filter: ReceiptsLogPruneConfig(
1299                std::iter::once((Address::ZERO, PruneMode::Full)).collect(),
1300            ),
1301            ..Default::default()
1302        };
1303        let pf = create_test_provider_factory().with_prune_modes(prune_modes);
1304        init_genesis(&pf).unwrap();
1305        let folder = tempdir().unwrap();
1306        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1307
1308        let provider = pf.database_provider_rw().unwrap();
1309        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1310        provider.commit().unwrap();
1311
1312        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1313        let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1314            stream,
1315            &pf,
1316            &mut hash_collector,
1317            None,
1318            true,
1319            &|_| true,
1320        );
1321
1322        assert!(result.is_err());
1323    }
1324
1325    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1326    async fn receipt_import_rejects_a_repair_starting_before_byzantium() {
1327        let pf = create_test_provider_factory();
1328        init_genesis(&pf).unwrap();
1329        let folder = tempdir().unwrap();
1330        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1331
1332        let provider = pf.database_provider_rw().unwrap();
1333        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(2)).unwrap();
1334        provider.commit().unwrap();
1335
1336        // Receipts resume at block 1, before this chain's Byzantium activation at block 2.
1337        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1338        let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1339            stream,
1340            &pf,
1341            &mut hash_collector,
1342            None,
1343            true,
1344            &|number| number >= 2,
1345        );
1346
1347        assert!(result.is_err());
1348    }
1349
1350    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1351    async fn receipt_import_errors_when_the_source_stops_short() {
1352        let pf = create_test_provider_factory();
1353        init_genesis(&pf).unwrap();
1354        let folder = tempdir().unwrap();
1355        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1356
1357        let provider = pf.database_provider_rw().unwrap();
1358        provider.save_stage_checkpoint(StageId::Execution, StageCheckpoint::new(10)).unwrap();
1359        provider.commit().unwrap();
1360
1361        // The source only carries blocks 1 and 2, far short of the executed range.
1362        let stream = futures_util::stream::iter(vec![Ok(TestMeta { marked: Cell::new(false) })]);
1363        let result = import::<TestEraWithEmptyReceipts, _, _, _, Block, _, _>(
1364            stream,
1365            &pf,
1366            &mut hash_collector,
1367            None,
1368            true,
1369            &|_| true,
1370        );
1371
1372        assert!(result.is_err());
1373    }
1374
1375    #[test]
1376    fn process_does_not_mark_partially_consumed_file_processed() {
1377        let pf = create_test_provider_factory();
1378        init_genesis(&pf).unwrap();
1379
1380        let static_file_provider = pf.static_file_provider();
1381        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1382        let provider = pf.database_provider_rw().unwrap();
1383        let folder = tempdir().unwrap();
1384        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1385        let meta = TestMeta { marked: Cell::new(false) };
1386
1387        let height = process::<TestEra, _, Block, _, _>(
1388            &meta,
1389            &mut writer,
1390            None,
1391            &provider,
1392            &mut hash_collector,
1393            0..=1,
1394            ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1395        )
1396        .unwrap();
1397
1398        assert_eq!(height, 1);
1399        assert!(!meta.marked.get());
1400    }
1401
1402    #[test]
1403    fn process_iter_rejects_non_contiguous_blocks() {
1404        let pf = create_test_provider_factory();
1405        init_genesis(&pf).unwrap();
1406
1407        let static_file_provider = pf.static_file_provider();
1408        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1409        let provider = pf.database_provider_rw().unwrap();
1410        let folder = tempdir().unwrap();
1411        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1412
1413        // Genesis DB sits at height 0, but the first block is 5: a gap that must be rejected
1414        // rather than appended (as a pre-merge `.era` import would otherwise produce).
1415        let blocks = [5u64, 6].into_iter().map(|number| {
1416            Ok((Header { number, ..Default::default() }, BlockBody::default(), None))
1417        });
1418
1419        let result = process_iter::<_, Block, _, _>(
1420            blocks,
1421            &mut writer,
1422            None,
1423            &provider,
1424            &mut hash_collector,
1425            0..,
1426            ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1427        );
1428
1429        assert!(result.is_err());
1430    }
1431
1432    #[test]
1433    fn process_iter_errors_when_receipts_missing() {
1434        let pf = create_test_provider_factory();
1435        init_genesis(&pf).unwrap();
1436
1437        let static_file_provider = pf.static_file_provider();
1438        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1439        let mut receipts_writer =
1440            static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1441        let provider = pf.database_provider_rw().unwrap();
1442        let folder = tempdir().unwrap();
1443        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1444
1445        // `TestEra` never yields receipts; requesting them must fail rather than silently import
1446        // headers/bodies without them.
1447        let blocks: Vec<
1448            eyre::Result<EraBlock<Header, BlockBody, reth_ethereum_primitives::Receipt>>,
1449        > = vec![Ok((Header { number: 1, ..Default::default() }, BlockBody::default(), None))];
1450
1451        let result = process_iter::<_, Block, _, _>(
1452            blocks.into_iter(),
1453            &mut writer,
1454            Some(&mut receipts_writer),
1455            &provider,
1456            &mut hash_collector,
1457            0..,
1458            ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1459        );
1460
1461        assert!(result.is_err());
1462    }
1463
1464    #[test]
1465    fn process_errors_on_receipt_count_mismatch() {
1466        let pf = create_test_provider_factory();
1467        init_genesis(&pf).unwrap();
1468
1469        let static_file_provider = pf.static_file_provider();
1470        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1471        let mut receipts_writer =
1472            static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1473        let provider = pf.database_provider_rw().unwrap();
1474        let folder = tempdir().unwrap();
1475        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1476        let meta = TestMeta { marked: Cell::new(false) };
1477
1478        // One receipt for a transaction-less body: a count mismatch that must be rejected rather
1479        // than silently misaligning the Receipts static file against Transactions.
1480        let result = process::<TestEraWithMismatchedReceipts, _, Block, _, _>(
1481            &meta,
1482            &mut writer,
1483            Some(&mut receipts_writer),
1484            &provider,
1485            &mut hash_collector,
1486            0..,
1487            ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1488        );
1489
1490        assert!(result.is_err());
1491    }
1492
1493    #[test]
1494    fn save_stage_checkpoints_leaves_execution_unset() {
1495        let pf = create_test_provider_factory();
1496        init_genesis(&pf).unwrap();
1497        let provider = pf.database_provider_rw().unwrap();
1498
1499        let execution_before = provider.get_stage_checkpoint(StageId::Execution).unwrap();
1500        save_stage_checkpoints(&provider, 0, 10, 10, 10).unwrap();
1501
1502        assert_eq!(
1503            provider.get_stage_checkpoint(StageId::Headers).unwrap().map(|c| c.block_number),
1504            Some(10)
1505        );
1506        assert_eq!(
1507            provider.get_stage_checkpoint(StageId::Bodies).unwrap().map(|c| c.block_number),
1508            Some(10)
1509        );
1510        // Receipt import doesn't produce state, so it must not advance Execution.
1511        assert_eq!(provider.get_stage_checkpoint(StageId::Execution).unwrap(), execution_before);
1512    }
1513
1514    #[test]
1515    fn backfills_receipts_onto_existing_headers() {
1516        let pf = create_test_provider_factory();
1517        init_genesis(&pf).unwrap();
1518        let static_file_provider = pf.static_file_provider();
1519        let folder = tempdir().unwrap();
1520        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1521
1522        // First pass: import headers and bodies only, no receipts.
1523        {
1524            let provider = pf.database_provider_rw().unwrap();
1525            // The writer holds the Headers segment lock, which `provider.commit()` also takes when
1526            // it finalizes every segment, so it must be released first.
1527            {
1528                let mut writer =
1529                    static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1530                let meta = TestMeta { marked: Cell::new(false) };
1531                process::<TestEra, _, Block, _, _>(
1532                    &meta,
1533                    &mut writer,
1534                    None,
1535                    &provider,
1536                    &mut hash_collector,
1537                    0..=2,
1538                    ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1539                )
1540                .unwrap();
1541                writer.commit().unwrap();
1542            }
1543            provider.commit().unwrap();
1544        }
1545        assert_eq!(
1546            static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers),
1547            Some(2)
1548        );
1549        // Receipts still only cover genesis; blocks 1 and 2 have headers but no receipts yet.
1550        assert_eq!(
1551            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1552            Some(0)
1553        );
1554
1555        // Second pass: backfill receipts for the already-imported headers (`headers_tip = 2`).
1556        {
1557            let provider = pf.database_provider_rw().unwrap();
1558            {
1559                let mut writer =
1560                    static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1561                let mut receipts_writer =
1562                    static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1563                let meta = TestMeta { marked: Cell::new(false) };
1564                process::<TestEraWithEmptyReceipts, _, Block, _, _>(
1565                    &meta,
1566                    &mut writer,
1567                    Some(&mut receipts_writer),
1568                    &provider,
1569                    &mut hash_collector,
1570                    0..,
1571                    ImportPolicy { headers_tip: 2, is_receipt_verifiable: &|_| false },
1572                )
1573                .unwrap();
1574                receipts_writer.commit().unwrap();
1575                writer.commit().unwrap();
1576            }
1577            provider.commit().unwrap();
1578        }
1579
1580        // Receipts now cover the previously header-only range, and headers weren't re-appended.
1581        assert_eq!(
1582            static_file_provider.get_highest_static_file_block(StaticFileSegment::Receipts),
1583            Some(2)
1584        );
1585        assert_eq!(
1586            static_file_provider.get_highest_static_file_block(StaticFileSegment::Headers),
1587            Some(2)
1588        );
1589    }
1590
1591    #[test]
1592    fn process_iter_persists_verified_receipts() {
1593        let pf = create_test_provider_factory();
1594        init_genesis(&pf).unwrap();
1595        let static_file_provider = pf.static_file_provider();
1596        let folder = tempdir().unwrap();
1597        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1598
1599        let (header, body, receipt) = block_with_one_receipt(1);
1600        let provider = pf.database_provider_rw().unwrap();
1601        {
1602            let mut writer =
1603                static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1604            let mut receipts_writer =
1605                static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1606
1607            process_iter::<_, Block, _, _>(
1608                std::iter::once(Ok((header, body, Some(vec![receipt.clone()])))),
1609                &mut writer,
1610                Some(&mut receipts_writer),
1611                &provider,
1612                &mut hash_collector,
1613                0..,
1614                ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1615            )
1616            .unwrap();
1617
1618            receipts_writer.commit().unwrap();
1619            writer.commit().unwrap();
1620        }
1621        provider.commit().unwrap();
1622
1623        let provider = pf.provider().unwrap();
1624        assert_eq!(provider.receipts_by_block(1.into()).unwrap(), Some(vec![receipt]));
1625    }
1626
1627    #[test]
1628    fn process_iter_rejects_receipts_the_header_does_not_commit_to() {
1629        let pf = create_test_provider_factory();
1630        init_genesis(&pf).unwrap();
1631        let static_file_provider = pf.static_file_provider();
1632        let folder = tempdir().unwrap();
1633        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1634
1635        // Right receipt count, wrong contents: only the recomputed root catches this.
1636        let (header, body, receipt) = block_with_one_receipt(1);
1637        let tampered = Receipt { cumulative_gas_used: 42_000, ..receipt };
1638
1639        let provider = pf.database_provider_rw().unwrap();
1640        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1641        let mut receipts_writer =
1642            static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1643
1644        let result = process_iter::<_, Block, _, _>(
1645            std::iter::once(Ok((header, body, Some(vec![tampered])))),
1646            &mut writer,
1647            Some(&mut receipts_writer),
1648            &provider,
1649            &mut hash_collector,
1650            0..,
1651            ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| true },
1652        );
1653
1654        assert!(result.is_err());
1655    }
1656
1657    #[test]
1658    fn decodes_post_byzantium_era1_receipts() {
1659        let tuple = era1_block_tuple(4_370_000, vec![era1_receipt(Eip658Value::Eip658(true))]);
1660
1661        let (_, _, receipts) =
1662            decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true).unwrap();
1663
1664        let receipts = receipts.unwrap();
1665        assert_eq!(receipts.len(), 1);
1666        assert!(receipts[0].success);
1667        assert_eq!(receipts[0].cumulative_gas_used, 21_000);
1668    }
1669
1670    #[test]
1671    fn decodes_typed_ere_receipts_into_node_receipts() {
1672        let logs = vec![Log::new_unchecked(
1673            Address::repeat_byte(0x11),
1674            vec![B256::repeat_byte(0x22)],
1675            Bytes::from_static(b"typed"),
1676        )];
1677        let slim = vec![
1678            SlimReceipt {
1679                tx_type: TxType::Eip2930,
1680                status: Eip658Value::Eip658(true),
1681                cumulative_gas_used: 21_000,
1682                logs: logs.clone(),
1683            },
1684            SlimReceipt {
1685                tx_type: TxType::Eip1559,
1686                status: Eip658Value::Eip658(false),
1687                cumulative_gas_used: 42_000,
1688                logs: vec![],
1689            },
1690        ];
1691        let tuple = ere_block_tuple(12_965_000, &slim);
1692
1693        let (_, _, receipts) =
1694            Ere::decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true)
1695                .unwrap();
1696
1697        assert_eq!(
1698            receipts,
1699            Some(vec![
1700                Receipt {
1701                    tx_type: TxType::Eip2930,
1702                    success: true,
1703                    cumulative_gas_used: 21_000,
1704                    logs,
1705                },
1706                Receipt {
1707                    tx_type: TxType::Eip1559,
1708                    success: false,
1709                    cumulative_gas_used: 42_000,
1710                    logs: vec![],
1711                },
1712            ])
1713        );
1714    }
1715
1716    #[test]
1717    fn rejects_pre_byzantium_era1_receipts() {
1718        // Mainnet's first transaction: its receipt commits to a post-state root.
1719        let tuple = era1_block_tuple(
1720            46_147,
1721            vec![era1_receipt(Eip658Value::PostState(B256::repeat_byte(1)))],
1722        );
1723
1724        let err = decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), true)
1725            .unwrap_err()
1726            .to_string();
1727
1728        assert!(err.contains("pre-Byzantium"), "unexpected error: {err}");
1729        assert!(err.contains("46147"), "error should name the offending block: {err}");
1730    }
1731
1732    #[test]
1733    fn skips_pre_byzantium_receipts_when_not_requested() {
1734        // A header-only import of the same file must stay unaffected.
1735        let tuple = era1_block_tuple(
1736            46_147,
1737            vec![era1_receipt(Eip658Value::PostState(B256::repeat_byte(1)))],
1738        );
1739
1740        let (header, _, receipts) =
1741            decode_with_receipts::<Header, BlockBody, Receipt, E2sError>(Ok(tuple), false).unwrap();
1742
1743        assert_eq!(header.number, 46_147);
1744        assert!(receipts.is_none());
1745    }
1746
1747    #[test]
1748    fn backfill_rejects_a_header_that_differs_from_the_persisted_one() {
1749        let pf = create_test_provider_factory();
1750        init_genesis(&pf).unwrap();
1751        let static_file_provider = pf.static_file_provider();
1752        let folder = tempdir().unwrap();
1753        let mut hash_collector = Collector::new(4096, Some(folder.path().to_owned()));
1754
1755        let provider = pf.database_provider_rw().unwrap();
1756        {
1757            let mut writer =
1758                static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1759            let meta = TestMeta { marked: Cell::new(false) };
1760            process::<TestEra, _, Block, _, _>(
1761                &meta,
1762                &mut writer,
1763                None,
1764                &provider,
1765                &mut hash_collector,
1766                0..=2,
1767                ImportPolicy { headers_tip: 0, is_receipt_verifiable: &|_| false },
1768            )
1769            .unwrap();
1770            writer.commit().unwrap();
1771        }
1772        provider.commit().unwrap();
1773
1774        // Same height and receipt count as the persisted block, but a different header.
1775        let provider = pf.database_provider_rw().unwrap();
1776        let mut writer = static_file_provider.latest_writer(StaticFileSegment::Headers).unwrap();
1777        let mut receipts_writer =
1778            static_file_provider.latest_writer(StaticFileSegment::Receipts).unwrap();
1779
1780        let result = process_iter::<_, Block, _, _>(
1781            std::iter::once(Ok((
1782                Header { number: 1, gas_limit: 42, ..Default::default() },
1783                BlockBody::default(),
1784                Some(vec![]),
1785            ))),
1786            &mut writer,
1787            Some(&mut receipts_writer),
1788            &provider,
1789            &mut hash_collector,
1790            0..,
1791            ImportPolicy { headers_tip: 2, is_receipt_verifiable: &|_| false },
1792        );
1793
1794        assert!(result.is_err());
1795    }
1796
1797    #[test]
1798    fn verify_receipts_checks_the_bloom_even_without_the_root() {
1799        let receipts = vec![Receipt {
1800            tx_type: TxType::Legacy,
1801            success: true,
1802            cumulative_gas_used: 21_000,
1803            logs: vec![Log::new_unchecked(
1804                Address::ZERO,
1805                vec![B256::repeat_byte(1)],
1806                Bytes::default(),
1807            )],
1808        }];
1809
1810        // The header commits to no logs at all, so the bloom cannot match.
1811        assert!(verify_receipts(&Header::default(), &receipts, false).is_err());
1812    }
1813}