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