Skip to main content

reth_era_utils/export/
era1.rs

1//! `.era1` block-history writer.
2
3use super::{ChunkAccumulator, EraBlockWriter, ExportBlock};
4use crate::Era1;
5use alloy_consensus::{BlockHeader, TxReceipt};
6use alloy_primitives::{B256, U256};
7use alloy_rlp::Encodable;
8use eyre::{eyre, Result};
9use reth_era::{
10    common::file_ops::{EraFileId, StreamWriter},
11    e2s::types::{Header, IndexEntry},
12    era1::{
13        file::Era1Writer,
14        types::{
15            execution::{
16                Accumulator, BlockTuple, CompressedBody, CompressedHeader, CompressedReceipts,
17                HeaderRecord, TotalDifficulty, MAX_BLOCKS_PER_ERA1,
18            },
19            group::{BlockIndex, Era1Id},
20        },
21    },
22};
23use reth_primitives_traits::Receipt;
24use std::path::{Path, PathBuf};
25
26impl EraBlockWriter for Era1 {
27    fn write_file<H, B, R>(
28        network: &str,
29        max_blocks_per_file: u64,
30        blocks: &[ExportBlock<H, B, R>],
31        dir: &Path,
32    ) -> Result<PathBuf>
33    where
34        H: BlockHeader + Encodable,
35        B: Encodable,
36        R: Receipt,
37    {
38        let accumulator = super::accumulator::<Accumulator, _, _, _>(blocks)?;
39        let file_path = dir.join(file_name(network, max_blocks_per_file, blocks, &accumulator));
40
41        let mut writer = Era1Writer::new(std::fs::File::create(&file_path)?);
42        writer.write_version()?;
43
44        // `era1` streams its records block by block; offsets track the running write position and
45        // are rebased onto the block-index record once that record's position is known.
46        let mut offsets = Vec::<i64>::with_capacity(blocks.len());
47        let mut position = Header::SIZE as i64; // past the leading version record
48        for block in blocks {
49            let tuple = compress_block(block)?;
50            offsets.push(position);
51            position += tuple.size() as i64;
52            writer.write_block(&tuple)?;
53        }
54
55        let index_position = position + accumulator.to_entry().size() as i64;
56        let relative: Vec<i64> = offsets.iter().map(|&abs| abs - index_position).collect();
57
58        writer.write_accumulator(&accumulator)?;
59        writer.write_block_index(&BlockIndex::new(blocks[0].header.number(), relative))?;
60        writer.flush()?;
61
62        Ok(file_path)
63    }
64}
65
66/// Compresses one block into an `era1` [`BlockTuple`] (header, body, bloom-bearing receipts,
67/// cumulative total difficulty).
68fn compress_block<H, B, R>(block: &ExportBlock<H, B, R>) -> Result<BlockTuple>
69where
70    H: BlockHeader + Encodable,
71    B: Encodable,
72    R: Receipt,
73{
74    let header = CompressedHeader::from_header(&block.header)?;
75    let body = CompressedBody::from_body(&block.body)?;
76    let receipts_with_bloom: Vec<_> =
77        block.receipts.iter().map(|r| TxReceipt::with_bloom_ref(r)).collect();
78    let receipts = CompressedReceipts::from_encodable_list(&receipts_with_bloom)
79        .map_err(|e| eyre!("Failed to compress receipts: {e}"))?;
80
81    Ok(BlockTuple::new(header, body, receipts, TotalDifficulty::new(block.total_difficulty)))
82}
83
84impl ChunkAccumulator for Accumulator {
85    fn from_pairs(records: &[(B256, U256)]) -> Result<Self> {
86        let records: Vec<HeaderRecord> = records
87            .iter()
88            .map(|&(block_hash, total_difficulty)| HeaderRecord { block_hash, total_difficulty })
89            .collect();
90        Self::from_header_records(&records).map_err(|e| eyre!("Failed to compute accumulator: {e}"))
91    }
92}
93
94/// Builds the output filename, taking the short hash from the accumulator root.
95fn file_name<H: BlockHeader, B, R>(
96    network: &str,
97    max_blocks_per_file: u64,
98    blocks: &[ExportBlock<H, B, R>],
99    accumulator: &Accumulator,
100) -> String {
101    let file_hash = super::short_hash(accumulator.root);
102    let id =
103        Era1Id::new(network, blocks[0].header.number(), blocks.len() as u32).with_hash(file_hash);
104    // Custom block-per-file exports tag the era count into the filename.
105    if max_blocks_per_file == MAX_BLOCKS_PER_ERA1 as u64 {
106        id.to_file_name()
107    } else {
108        id.with_era_count().to_file_name()
109    }
110}