Skip to main content

reth_era/ere/types/
execution.rs

1//! Execution layer specific types for `.ere` files
2//!
3//! Contains implementations for compressed execution layer data structures:
4//! - [`CompressedHeader`] - Block header
5//! - [`CompressedBody`] - Block body
6//! - [`CompressedSlimReceipts`] - Block receipts
7//! - [`TotalDifficulty`] - Block total difficulty
8//!
9//! These types use Snappy compression to match the specification.
10//!
11//! See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/ere.md>
12
13use crate::{
14    common::{
15        compression::{snappy_compress, snappy_decompress, SnappyRlpCodec},
16        decode::DecodeCompressedRlp,
17    },
18    e2s::{error::E2sError, types::Entry},
19};
20use alloy_consensus::{Block, BlockBody, Eip658Value, Header, Receipt, ReceiptEnvelope, TxType};
21use alloy_primitives::{Log, B256, U256};
22use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
23use sha2::{Digest, Sha256};
24
25// ERE-specific constants
26/// `CompressedHeader` record type
27pub const COMPRESSED_HEADER: [u8; 2] = [0x03, 0x00];
28
29/// `CompressedBody` record type
30pub const COMPRESSED_BODY: [u8; 2] = [0x04, 0x00];
31
32/// `CompressedSlimReceipts` record type (0x0a00)
33/// Slim receipts exclude bloom filters to optimize storage.
34pub const COMPRESSED_SLIM_RECEIPTS: [u8; 2] = [0x0a, 0x00];
35
36/// `Proof` record type (0x0b00)
37/// Format: `snappyFramed(rlp([proof-type, ssz(proof-object)]))`
38pub const PROOF: [u8; 2] = [0x0b, 0x00];
39
40/// `TotalDifficulty` record type
41pub const TOTAL_DIFFICULTY: [u8; 2] = [0x06, 0x00];
42
43/// `Accumulator` record type
44pub const ACCUMULATOR: [u8; 2] = [0x07, 0x00];
45
46/// Maximum number of blocks in an `ERE` file, limited by accumulator size.
47pub const MAX_BLOCKS_PER_ERE: usize = crate::common::MAX_ENTRIES_PER_ERA as usize;
48
49/// Compressed block header using `snappyFramed(rlp(header))`
50#[derive(Debug, Clone)]
51pub struct CompressedHeader {
52    /// The compressed data
53    pub data: Vec<u8>,
54}
55
56impl CompressedHeader {
57    /// Create a new [`CompressedHeader`] from compressed data
58    pub const fn new(data: Vec<u8>) -> Self {
59        Self { data }
60    }
61
62    /// Create from RLP-encoded header by compressing it with Snappy framed encoding
63    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
64        Ok(Self { data: snappy_compress(rlp_data)? })
65    }
66
67    /// Decompress to get the original RLP-encoded header
68    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
69        snappy_decompress(&self.data)
70    }
71
72    /// Convert to an [`Entry`]
73    pub fn to_entry(&self) -> Entry {
74        Entry::new(COMPRESSED_HEADER, self.data.clone())
75    }
76
77    /// Create from an [`Entry`]
78    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
79        entry.ensure_type(COMPRESSED_HEADER, "CompressedHeader")?;
80        Ok(Self { data: entry.data.clone() })
81    }
82
83    /// Decode this compressed header into an `alloy_consensus::Header`
84    pub fn decode_header(&self) -> Result<Header, E2sError> {
85        self.decode()
86    }
87
88    /// Create a [`CompressedHeader`] from a header
89    pub fn from_header<H: Encodable>(header: &H) -> Result<Self, E2sError> {
90        let encoder = SnappyRlpCodec::new();
91        let compressed = encoder.encode(header)?;
92        Ok(Self::new(compressed))
93    }
94}
95
96impl DecodeCompressedRlp for CompressedHeader {
97    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
98        let decoder = SnappyRlpCodec::<T>::new();
99        decoder.decode(&self.data)
100    }
101}
102
103/// Compressed block body using `snappyFramed(rlp(body))`
104#[derive(Debug, Clone)]
105pub struct CompressedBody {
106    /// The compressed data
107    pub data: Vec<u8>,
108}
109
110impl CompressedBody {
111    /// Create a new [`CompressedBody`] from compressed data
112    pub const fn new(data: Vec<u8>) -> Self {
113        Self { data }
114    }
115
116    /// Create from RLP-encoded body by compressing it with Snappy framed encoding
117    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
118        Ok(Self { data: snappy_compress(rlp_data)? })
119    }
120
121    /// Decompress to get the original RLP-encoded body
122    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
123        snappy_decompress(&self.data)
124    }
125
126    /// Convert to an [`Entry`]
127    pub fn to_entry(&self) -> Entry {
128        Entry::new(COMPRESSED_BODY, self.data.clone())
129    }
130
131    /// Create from an [`Entry`]
132    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
133        entry.ensure_type(COMPRESSED_BODY, "CompressedBody")?;
134        Ok(Self { data: entry.data.clone() })
135    }
136
137    /// Decode this [`CompressedBody`] into an `alloy_consensus::BlockBody`
138    pub fn decode_body<T: Decodable, H: Decodable>(&self) -> Result<BlockBody<T, H>, E2sError> {
139        let decompressed = self.decompress()?;
140        Self::decode_body_from_decompressed(&decompressed)
141    }
142
143    /// Decode decompressed body data into an `alloy_consensus::BlockBody`
144    pub fn decode_body_from_decompressed<T: Decodable, H: Decodable>(
145        data: &[u8],
146    ) -> Result<BlockBody<T, H>, E2sError> {
147        alloy_rlp::decode_exact::<BlockBody<T, H>>(data)
148            .map_err(|e| E2sError::Rlp(format!("Failed to decode RLP data: {e}")))
149    }
150
151    /// Create a [`CompressedBody`] from a block body (e.g.  `alloy_consensus::BlockBody`)
152    pub fn from_body<B: Encodable>(body: &B) -> Result<Self, E2sError> {
153        let encoder = SnappyRlpCodec::new();
154        let compressed = encoder.encode(body)?;
155        Ok(Self::new(compressed))
156    }
157}
158
159impl DecodeCompressedRlp for CompressedBody {
160    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
161        let decoder = SnappyRlpCodec::<T>::new();
162        decoder.decode(&self.data)
163    }
164}
165
166/// Compressed slim receipts using `snappyFramed(rlp(...))`.
167///
168/// Slim receipts exclude bloom filters to optimize storage.
169/// Format: `snappyFramed(rlp([tx-type, post-state-or-status, cumulative-gas, logs]))`
170#[derive(Debug, Clone)]
171pub struct CompressedSlimReceipts {
172    /// The compressed data
173    pub data: Vec<u8>,
174}
175
176impl CompressedSlimReceipts {
177    /// Create a new [`CompressedSlimReceipts`] from compressed data
178    pub const fn new(data: Vec<u8>) -> Self {
179        Self { data }
180    }
181
182    /// Create from RLP-encoded slim receipts by compressing with Snappy framed encoding
183    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
184        Ok(Self { data: snappy_compress(rlp_data)? })
185    }
186
187    /// Decompress to get the original RLP-encoded slim receipts
188    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
189        snappy_decompress(&self.data)
190    }
191
192    /// Convert to an [`Entry`]
193    pub fn to_entry(&self) -> Entry {
194        Entry::new(COMPRESSED_SLIM_RECEIPTS, self.data.clone())
195    }
196
197    /// Create from an [`Entry`]
198    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
199        entry.ensure_type(COMPRESSED_SLIM_RECEIPTS, "CompressedSlimReceipts")?;
200        Ok(Self { data: entry.data.clone() })
201    }
202
203    /// Decode this [`CompressedSlimReceipts`] into the given type
204    pub fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
205        let decoder = SnappyRlpCodec::<T>::new();
206        decoder.decode(&self.data)
207    }
208
209    /// Create [`CompressedSlimReceipts`] from an encodable type
210    pub fn from_encodable<T: Encodable>(data: &T) -> Result<Self, E2sError> {
211        let encoder = SnappyRlpCodec::<T>::new();
212        let compressed = encoder.encode(data)?;
213        Ok(Self::new(compressed))
214    }
215
216    /// Encode and compress a list of slim receipts
217    pub fn from_encodable_list<T: Encodable>(receipts: &[T]) -> Result<Self, E2sError> {
218        let mut rlp_data = Vec::new();
219        alloy_rlp::encode_list(receipts, &mut rlp_data);
220        Self::from_rlp(&rlp_data)
221    }
222
223    /// Compress a block's slim receipts.
224    ///
225    /// [`SlimReceipt`] is the canonical slim receipt: its RLP encoding is the 4-element list
226    /// `[tx-type, post-state-or-status, cumulative-gas, logs]`, with no bloom filter.
227    pub fn from_receipts(receipts: &[SlimReceipt]) -> Result<Self, E2sError> {
228        Self::from_encodable_list(receipts)
229    }
230
231    /// Decompress and decode this entry into a block's slim receipts.
232    pub fn decode_receipts(&self) -> Result<Vec<SlimReceipt>, E2sError> {
233        self.decode()
234    }
235}
236
237impl DecodeCompressedRlp for CompressedSlimReceipts {
238    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
239        let decoder = SnappyRlpCodec::<T>::new();
240        decoder.decode(&self.data)
241    }
242}
243
244/// A slim execution receipt as stored in an `ERE` file.
245///
246/// Per the spec, the slim form is the 4-element RLP list
247/// `[tx-type, post-state-or-status, cumulative-gas, logs]` with **no bloom filter** (the bloom is
248/// recomputable from the logs). This is a thin wrapper over alloy's field types: [`Eip658Value`]
249/// captures both the pre-Byzantium 32-byte post-state root and the post-Byzantium boolean status,
250/// so a single type decodes receipts across every fork.
251#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
252pub struct SlimReceipt {
253    /// Transaction type (EIP-2718).
254    pub tx_type: TxType,
255    /// Post-state root (pre-Byzantium) or success status (post-Byzantium).
256    pub status: Eip658Value,
257    /// Cumulative gas used in the block up to and including this transaction.
258    pub cumulative_gas_used: u64,
259    /// Logs emitted by the transaction.
260    pub logs: Vec<Log>,
261}
262
263impl From<SlimReceipt> for ReceiptEnvelope {
264    /// Restores the bloom that the slim form omits, recomputing it from the logs.
265    fn from(receipt: SlimReceipt) -> Self {
266        let SlimReceipt { tx_type, status, cumulative_gas_used, logs } = receipt;
267        let receipt = Receipt { status, cumulative_gas_used, logs };
268
269        Self::from_typed(tx_type, receipt.with_bloom())
270    }
271}
272
273/// Proof type discriminant used inside the Proof entry's RLP envelope.
274///
275/// Maps to specific Portal Network proof objects.
276#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277#[repr(u8)]
278pub enum ProofType {
279    /// Pre-merge proof against the historical hashes accumulator
280    BlockProofHistoricalHashesAccumulator = 0,
281    /// Post-merge proof against historical roots
282    BlockProofHistoricalRoots = 1,
283    /// Capella-era proof against historical summaries
284    BlockProofHistoricalSummariesCapella = 2,
285    /// Deneb-era proof against historical summaries
286    BlockProofHistoricalSummariesDeneb = 3,
287}
288
289impl ProofType {
290    /// Convert from a raw byte value
291    pub const fn from_byte(value: u8) -> Option<Self> {
292        match value {
293            0 => Some(Self::BlockProofHistoricalHashesAccumulator),
294            1 => Some(Self::BlockProofHistoricalRoots),
295            2 => Some(Self::BlockProofHistoricalSummariesCapella),
296            3 => Some(Self::BlockProofHistoricalSummariesDeneb),
297            _ => None,
298        }
299    }
300
301    /// Convert to a raw byte value
302    pub const fn as_byte(self) -> u8 {
303        self as u8
304    }
305}
306
307/// A proof entry attesting to block validity against a trusted consensus layer header.
308///
309/// Format: `snappyFramed(rlp([proof-type, ssz(proof-object)]))`
310///
311/// Multiple proof types can coexist in the same file at fork boundaries.
312#[derive(Debug, Clone)]
313pub struct Proof {
314    /// The compressed data containing `rlp([proof-type, ssz(proof-object)])`
315    pub data: Vec<u8>,
316}
317
318impl Proof {
319    /// Create a new [`Proof`] from already-compressed data
320    pub const fn new(data: Vec<u8>) -> Self {
321        Self { data }
322    }
323
324    /// Encode a [`Proof`] from a proof type and raw SSZ-encoded proof object.
325    pub fn encode(proof_type: ProofType, ssz_proof: &[u8]) -> Result<Self, E2sError> {
326        // Build the list payload first so the RLP list header gets the exact length,
327        // regardless of how each item encodes.
328        let mut payload = Vec::new();
329        proof_type.as_byte().encode(&mut payload);
330        ssz_proof.encode(&mut payload);
331
332        let mut rlp_data = Vec::new();
333        alloy_rlp::Header { list: true, payload_length: payload.len() }.encode(&mut rlp_data);
334        rlp_data.extend_from_slice(&payload);
335
336        Ok(Self { data: snappy_compress(&rlp_data)? })
337    }
338
339    /// Decode the proof, returning `(proof_type, raw_ssz_proof_bytes)`.
340    pub fn decode(&self) -> Result<(ProofType, Vec<u8>), E2sError> {
341        let decompressed = snappy_decompress(&self.data)?;
342
343        let mut buf = decompressed.as_slice();
344        let header = alloy_rlp::Header::decode(&mut buf)
345            .map_err(|e| E2sError::Rlp(format!("Failed to decode proof RLP header: {e}")))?;
346        if !header.list {
347            return Err(E2sError::Rlp("Expected RLP list for Proof entry".to_string()));
348        }
349
350        // A proof is exactly the two-item list `[proof-type, ssz(proof-object)]`. Pin decoding to
351        // the list's own payload so nothing outside it slips through: bytes after the list end, or
352        // a third item inside it, both fail instead of being silently dropped.
353        if buf.len() != header.payload_length {
354            return Err(E2sError::Rlp(format!(
355                "Trailing bytes after Proof list: {} byte(s) beyond the list payload",
356                buf.len().saturating_sub(header.payload_length)
357            )));
358        }
359        let mut payload = &buf[..header.payload_length];
360
361        let proof_type_byte = u8::decode(&mut payload)
362            .map_err(|e| E2sError::Rlp(format!("Failed to decode proof type: {e}")))?;
363        let proof_type = ProofType::from_byte(proof_type_byte)
364            .ok_or_else(|| E2sError::Rlp(format!("Unknown proof type: {proof_type_byte}")))?;
365
366        let ssz_bytes = alloy_rlp::Header::decode_bytes(&mut payload, false)
367            .map_err(|e| E2sError::Rlp(format!("Failed to decode proof SSZ bytes: {e}")))?
368            .to_vec();
369
370        if !payload.is_empty() {
371            return Err(E2sError::Rlp("Unexpected extra items in Proof list".to_string()));
372        }
373
374        Ok((proof_type, ssz_bytes))
375    }
376
377    /// Convert to an [`Entry`]
378    pub fn to_entry(&self) -> Entry {
379        Entry::new(PROOF, self.data.clone())
380    }
381
382    /// Create from an [`Entry`]
383    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
384        entry.ensure_type(PROOF, "Proof")?;
385        Ok(Self { data: entry.data.clone() })
386    }
387}
388
389/// Total difficulty for a block
390#[derive(Debug, Clone)]
391pub struct TotalDifficulty {
392    /// The total difficulty as U256
393    pub value: U256,
394}
395
396impl TotalDifficulty {
397    /// Create a new [`TotalDifficulty`] from a U256 value
398    pub const fn new(value: U256) -> Self {
399        Self { value }
400    }
401
402    /// Convert to an [`Entry`]
403    pub fn to_entry(&self) -> Entry {
404        // ere spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
405        let data = self.value.to_le_bytes::<32>().to_vec();
406        Entry::new(TOTAL_DIFFICULTY, data)
407    }
408
409    /// Create from an [`Entry`]
410    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
411        entry.ensure_type(TOTAL_DIFFICULTY, "TotalDifficulty")?;
412
413        if entry.data.len() != 32 {
414            return Err(E2sError::Ssz(format!(
415                "Invalid data length for TotalDifficulty: expected 32, got {}",
416                entry.data.len()
417            )));
418        }
419
420        // ere spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
421        let value = U256::from_le_slice(&entry.data);
422
423        Ok(Self { value })
424    }
425}
426
427/// Accumulator is computed by constructing an SSZ list of header-records
428/// and calculating the `hash_tree_root`
429#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct Accumulator {
431    /// The accumulator root hash
432    pub root: B256,
433}
434
435impl Accumulator {
436    /// Create a new [`Accumulator`] from a root hash
437    pub const fn new(root: B256) -> Self {
438        Self { root }
439    }
440
441    /// Convert to an [`Entry`]
442    pub fn to_entry(&self) -> Entry {
443        Entry::new(ACCUMULATOR, self.root.to_vec())
444    }
445
446    /// Create from an [`Entry`]
447    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
448        entry.ensure_type(ACCUMULATOR, "Accumulator")?;
449
450        if entry.data.len() != 32 {
451            return Err(E2sError::Ssz(format!(
452                "Invalid data length for Accumulator: expected 32, got {}",
453                entry.data.len()
454            )));
455        }
456
457        let mut root = [0u8; 32];
458        root.copy_from_slice(&entry.data);
459
460        Ok(Self { root: B256::from(root) })
461    }
462
463    /// Compute the accumulator from a list of header records.
464    ///
465    /// Implements `hash_tree_root(List[HeaderRecord, 8192])` per the spec:
466    /// - Each leaf is `sha256(block_hash || total_difficulty_le_bytes32)`
467    /// - Leaves are padded to `MAX_BLOCKS_PER_ERE` (8192) with zero hashes
468    /// - Binary Merkle tree is computed bottom-up
469    /// - Final root is `sha256(merkle_root || le_bytes32(actual_count))`
470    ///
471    /// Returns `Err` if `records` exceeds [`MAX_BLOCKS_PER_ERE`].
472    pub fn from_header_records(records: &[HeaderRecord]) -> Result<Self, E2sError> {
473        let capacity = MAX_BLOCKS_PER_ERE;
474
475        if records.len() > capacity {
476            return Err(E2sError::Ssz(format!(
477                "Too many header records: got {}, max {}",
478                records.len(),
479                capacity
480            )));
481        }
482
483        // Compute leaf hash for each header record
484        let mut leaves = Vec::with_capacity(capacity);
485        for record in records {
486            let mut data = [0u8; 64];
487            data[..32].copy_from_slice(record.block_hash.as_slice());
488            data[32..].copy_from_slice(&record.total_difficulty.to_le_bytes::<32>());
489            leaves.push(<[u8; 32]>::from(Sha256::digest(data)));
490        }
491
492        // Pad to capacity with zero hashes
493        leaves.resize(capacity, [0u8; 32]);
494
495        // Binary Merkle tree bottom-up (capacity is always a power of two)
496        while leaves.len() > 1 {
497            let mut next_level = Vec::with_capacity(leaves.len() / 2);
498            for pair in leaves.as_chunks::<2>().0 {
499                let mut data = [0u8; 64];
500                data[..32].copy_from_slice(&pair[0]);
501                data[32..].copy_from_slice(&pair[1]);
502                next_level.push(<[u8; 32]>::from(Sha256::digest(data)));
503            }
504            leaves = next_level;
505        }
506
507        let merkle_root = leaves[0];
508
509        // mix_in_length: sha256(merkle_root || le_bytes32(actual_length))
510        let mut mix = [0u8; 64];
511        mix[..32].copy_from_slice(&merkle_root);
512        let length = records.len() as u64;
513        mix[32..40].copy_from_slice(&length.to_le_bytes());
514        // remaining bytes stay zero (uint256 LE padding)
515
516        Ok(Self { root: B256::from(<[u8; 32]>::from(Sha256::digest(mix))) })
517    }
518}
519
520/// The minimal per-block commitment used to build the accumulator.
521///
522/// This is **not** a block header: it is the 64-byte leaf
523/// `{ block-hash: Bytes32, total-difficulty: Uint256 }` that
524/// [`Accumulator::from_header_records`] merkleizes as `hash_tree_root(List[HeaderRecord, 8192])`.
525/// The full header is stored separately as [`CompressedHeader`].
526///
527/// Only meaningful pre-merge, since `total-difficulty` stops advancing after the merge.
528#[derive(Debug, Clone)]
529pub struct HeaderRecord {
530    /// The canonical block hash, i.e. `keccak256(rlp(header))` — the hash *of* the full block
531    /// header, which serves as this leaf's identity in the accumulator.
532    pub block_hash: B256,
533    /// The **cumulative** total difficulty through this block (the running sum of every block's
534    /// difficulty up to and including it), not the header's own per-block `difficulty` field.
535    pub total_difficulty: U256,
536}
537
538/// A single block's components in an `ERE` file.
539///
540/// Only the header and body are mandatory; receipts, total difficulty, and the proof are optional,
541/// so subset profiles or post-merge blocks can omit them.
542/// [`component_count`](Self::component_count) reports how many are present, matching the file's
543/// `DynamicBlockIndex` `component-count`.
544///
545/// See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/ere.md#specification>
546#[derive(Debug, Clone)]
547pub struct BlockTuple {
548    /// Compressed block header
549    pub header: CompressedHeader,
550
551    /// Compressed block body
552    pub body: CompressedBody,
553
554    /// Compressed slim receipts, omitted by the `noreceipts` profile
555    pub receipts: Option<CompressedSlimReceipts>,
556
557    /// Total difficulty, absent once it stops advancing after the merge
558    pub total_difficulty: Option<TotalDifficulty>,
559
560    /// Proof of block validity, omitted by the `noproofs` profile
561    pub proof: Option<Proof>,
562}
563
564impl BlockTuple {
565    /// Create a new [`BlockTuple`] with only the mandatory header and body.
566    ///
567    /// Attach the optional components with [`with_receipts`](Self::with_receipts),
568    /// [`with_total_difficulty`](Self::with_total_difficulty), and
569    /// [`with_proof`](Self::with_proof).
570    pub const fn new(header: CompressedHeader, body: CompressedBody) -> Self {
571        Self { header, body, receipts: None, total_difficulty: None, proof: None }
572    }
573
574    /// Attach compressed slim receipts.
575    pub fn with_receipts(mut self, receipts: CompressedSlimReceipts) -> Self {
576        self.receipts = Some(receipts);
577        self
578    }
579
580    /// Attach the total difficulty.
581    pub const fn with_total_difficulty(mut self, total_difficulty: TotalDifficulty) -> Self {
582        self.total_difficulty = Some(total_difficulty);
583        self
584    }
585
586    /// Attach a validity proof.
587    pub fn with_proof(mut self, proof: Proof) -> Self {
588        self.proof = Some(proof);
589        self
590    }
591
592    /// Number of index components this block contributes: the mandatory header and body plus each
593    /// optional component present. Always in the range 2-5, matching the file's `component-count`.
594    pub const fn component_count(&self) -> u64 {
595        2 + self.receipts.is_some() as u64 +
596            self.total_difficulty.is_some() as u64 +
597            self.proof.is_some() as u64
598    }
599
600    /// Convert to an `alloy_consensus::Block`
601    pub fn to_alloy_block<T: Decodable>(&self) -> Result<Block<T>, E2sError> {
602        let header: Header = self.header.decode()?;
603        let body: BlockBody<T> = self.body.decode()?;
604
605        Ok(Block::new(header, body))
606    }
607
608    /// Create from an `alloy_consensus::Block`, attaching the given receipts and total difficulty.
609    pub fn from_alloy_block<T: Encodable, R: Encodable>(
610        block: &Block<T>,
611        receipts: &R,
612        total_difficulty: U256,
613    ) -> Result<Self, E2sError> {
614        let header = CompressedHeader::from_header(&block.header)?;
615        let body = CompressedBody::from_body(&block.body)?;
616
617        let compressed_receipts = CompressedSlimReceipts::from_encodable(receipts)?;
618
619        let difficulty = TotalDifficulty::new(total_difficulty);
620
621        Ok(Self::new(header, body)
622            .with_receipts(compressed_receipts)
623            .with_total_difficulty(difficulty))
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use crate::test_utils::{create_header, create_test_receipt, create_test_receipts};
631    use alloy_eips::eip4895::Withdrawals;
632    use alloy_primitives::{Bytes, U256};
633    use reth_ethereum_primitives::{Receipt, TxType};
634
635    #[test]
636    fn test_header_conversion_roundtrip() {
637        let header = create_header();
638
639        let compressed_header = CompressedHeader::from_header(&header).unwrap();
640
641        let decoded_header = compressed_header.decode_header().unwrap();
642
643        assert_eq!(header.number, decoded_header.number);
644        assert_eq!(header.difficulty, decoded_header.difficulty);
645        assert_eq!(header.timestamp, decoded_header.timestamp);
646        assert_eq!(header.gas_used, decoded_header.gas_used);
647        assert_eq!(header.parent_hash, decoded_header.parent_hash);
648        assert_eq!(header.base_fee_per_gas, decoded_header.base_fee_per_gas);
649    }
650
651    #[test]
652    fn test_block_body_conversion() {
653        let block_body: BlockBody<Bytes> =
654            BlockBody { transactions: vec![], ommers: vec![], withdrawals: None };
655
656        let compressed_body = CompressedBody::from_body(&block_body).unwrap();
657
658        let decoded_body: BlockBody<Bytes> = compressed_body.decode_body().unwrap();
659
660        assert_eq!(decoded_body.transactions.len(), 0);
661        assert_eq!(decoded_body.ommers.len(), 0);
662        assert_eq!(decoded_body.withdrawals, None);
663    }
664
665    #[test]
666    fn test_total_difficulty_roundtrip() {
667        let value = U256::from(123456789u64);
668
669        let total_difficulty = TotalDifficulty::new(value);
670
671        let entry = total_difficulty.to_entry();
672
673        assert_eq!(entry.entry_type, TOTAL_DIFFICULTY);
674
675        let recovered = TotalDifficulty::from_entry(&entry).unwrap();
676
677        assert_eq!(recovered.value, value);
678    }
679
680    #[test]
681    fn test_total_difficulty_ssz_le_encoding() {
682        // Verify that total-difficulty is encoded as SSZ uint256 (little-endian).
683        // See https://github.com/eth-clients/e2store-format-specs/blob/main/formats/ere.md
684        let value = U256::from(1u64);
685        let td = TotalDifficulty::new(value);
686        let entry = td.to_entry();
687
688        // Little-endian: least significant byte first [1, 0, 0, ..., 0]
689        assert_eq!(entry.data[0], 1, "First byte must be 1 (little-endian)");
690        assert_eq!(entry.data[31], 0, "Last byte must be 0 (little-endian)");
691    }
692
693    #[test]
694    fn test_compression_roundtrip() {
695        let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
696
697        // Test header compression/decompression
698        let compressed_header = CompressedHeader::from_rlp(&rlp_data).unwrap();
699        let decompressed = compressed_header.decompress().unwrap();
700        assert_eq!(decompressed, rlp_data);
701
702        // Test body compression/decompression
703        let compressed_body = CompressedBody::from_rlp(&rlp_data).unwrap();
704        let decompressed = compressed_body.decompress().unwrap();
705        assert_eq!(decompressed, rlp_data);
706
707        // Test receipts compression/decompression
708        let compressed_receipts = CompressedSlimReceipts::from_rlp(&rlp_data).unwrap();
709        let decompressed = compressed_receipts.decompress().unwrap();
710        assert_eq!(decompressed, rlp_data);
711    }
712
713    #[test]
714    fn test_block_tuple_with_data() {
715        // Create block with transactions and withdrawals
716        let header = create_header();
717
718        let transactions = vec![Bytes::from(vec![1, 2, 3, 4]), Bytes::from(vec![5, 6, 7, 8])];
719
720        let withdrawals = Some(Withdrawals(vec![]));
721
722        let block_body = BlockBody { transactions, ommers: vec![], withdrawals };
723
724        let block = Block::new(header, block_body);
725
726        let receipts: Vec<u8> = Vec::new();
727
728        let block_tuple =
729            BlockTuple::from_alloy_block(&block, &receipts, U256::from(123456u64)).unwrap();
730
731        // Convert back to Block
732        let decoded_block: Block<Bytes> = block_tuple.to_alloy_block().unwrap();
733
734        // Verify block components
735        assert_eq!(decoded_block.header.number, 100);
736        assert_eq!(decoded_block.body.transactions.len(), 2);
737        assert_eq!(decoded_block.body.transactions[0], Bytes::from(vec![1, 2, 3, 4]));
738        assert_eq!(decoded_block.body.transactions[1], Bytes::from(vec![5, 6, 7, 8]));
739        assert!(decoded_block.body.withdrawals.is_some());
740    }
741
742    #[test]
743    fn test_block_tuple_component_count() {
744        let base = BlockTuple::new(CompressedHeader::new(vec![1]), CompressedBody::new(vec![2]));
745        // Mandatory header + body only.
746        assert_eq!(base.component_count(), 2);
747
748        // Each optional component bumps the count, up to the 5-component maximum.
749        assert_eq!(
750            base.clone().with_receipts(CompressedSlimReceipts::new(vec![3])).component_count(),
751            3
752        );
753        let full = base
754            .with_receipts(CompressedSlimReceipts::new(vec![3]))
755            .with_total_difficulty(TotalDifficulty::new(U256::from(1u64)))
756            .with_proof(Proof::new(vec![4]));
757        assert_eq!(full.component_count(), 5);
758        assert!(full.receipts.is_some() && full.total_difficulty.is_some() && full.proof.is_some());
759    }
760
761    #[test]
762    fn test_single_receipt_compression_roundtrip() {
763        let test_receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
764
765        // Compress the receipt
766        let compressed_receipts = CompressedSlimReceipts::from_encodable(&test_receipt)
767            .expect("Failed to compress receipt");
768
769        // Verify compression
770        assert!(!compressed_receipts.data.is_empty());
771
772        // Decode the compressed receipt back
773        let decoded_receipt: Receipt =
774            compressed_receipts.decode().expect("Failed to decode compressed receipt");
775
776        // Verify that the decoded receipt matches the original
777        assert_eq!(decoded_receipt.tx_type, test_receipt.tx_type);
778        assert_eq!(decoded_receipt.success, test_receipt.success);
779        assert_eq!(decoded_receipt.cumulative_gas_used, test_receipt.cumulative_gas_used);
780        assert_eq!(decoded_receipt.logs.len(), test_receipt.logs.len());
781
782        // Verify each log
783        for (original_log, decoded_log) in test_receipt.logs.iter().zip(decoded_receipt.logs.iter())
784        {
785            assert_eq!(decoded_log.address, original_log.address);
786            assert_eq!(decoded_log.data.topics(), original_log.data.topics());
787        }
788    }
789
790    #[test]
791    fn test_slim_receipt_matches_spec_rlp() {
792        // Spec: CompressedSlimReceipts.data = snappyFramed(rlp([tx-type, status, cumulative-gas,
793        // logs])), with no bloom filter. Prove the inner RLP of `EthereumReceipt` is exactly that
794        // 4-element list, byte for byte.
795        let receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
796
797        let compressed = CompressedSlimReceipts::from_encodable(&receipt).unwrap();
798        let actual_rlp = compressed.decompress().unwrap();
799
800        // Hand-build rlp([tx-type, status, cumulative-gas, logs]) in spec field order.
801        let mut fields = Vec::new();
802        (receipt.tx_type as u8).encode(&mut fields);
803        receipt.success.encode(&mut fields);
804        receipt.cumulative_gas_used.encode(&mut fields);
805        receipt.logs.encode(&mut fields);
806        let mut expected = Vec::new();
807        alloy_rlp::Header { list: true, payload_length: fields.len() }.encode(&mut expected);
808        expected.extend_from_slice(&fields);
809
810        assert_eq!(
811            actual_rlp, expected,
812            "slim receipt RLP must be the 4-element list [tx-type, status, cumulative-gas, logs]"
813        );
814    }
815
816    #[test]
817    fn test_slim_receipts_typed_helpers() {
818        // Cover both status variants: post-Byzantium boolean status and a pre-Byzantium 32-byte
819        // post-state root, proving a single `SlimReceipt` type round-trips across forks.
820        let receipts = vec![
821            SlimReceipt {
822                tx_type: TxType::Eip1559,
823                status: Eip658Value::Eip658(true),
824                cumulative_gas_used: 21000,
825                logs: vec![],
826            },
827            SlimReceipt {
828                tx_type: TxType::Legacy,
829                status: Eip658Value::PostState(B256::repeat_byte(0xab)),
830                cumulative_gas_used: 42000,
831                logs: vec![],
832            },
833        ];
834
835        let compressed = CompressedSlimReceipts::from_receipts(&receipts).unwrap();
836        let decoded = compressed.decode_receipts().unwrap();
837
838        assert_eq!(decoded, receipts);
839    }
840
841    #[test]
842    fn test_accumulator_from_header_records_known_vectors() {
843        // Known-answer vectors computed from the SSZ spec:
844        //   hash_tree_root(List[HeaderRecord, 8192])
845        let expected_empty: B256 =
846            "4a8c3a07c8d23adc5bac61157555c3c784d53d9bc110c1370809bd23cd93777d".parse().unwrap();
847        let expected_single_zero: B256 =
848            "81fd641249670887a731386e756a7a1538dc781b1b0bf016889045d350812817".parse().unwrap();
849        let expected_single_nonzero: B256 =
850            "ada35c48d81117f4fd588554cd4c4752356336e84cb41106dea1ceb4cfac8799".parse().unwrap();
851
852        // Empty list
853        let acc_empty = Accumulator::from_header_records(&[]).unwrap();
854        assert_eq!(acc_empty.root, expected_empty);
855
856        // Single record with zero values
857        let records = vec![HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO }];
858        let acc = Accumulator::from_header_records(&records).unwrap();
859        assert_eq!(acc.root, expected_single_zero);
860
861        // Single record with non-zero values
862        let records2 = vec![HeaderRecord {
863            block_hash: B256::from([1u8; 32]),
864            total_difficulty: U256::from(100u64),
865        }];
866        let acc2 = Accumulator::from_header_records(&records2).unwrap();
867        assert_eq!(acc2.root, expected_single_nonzero);
868    }
869
870    #[test]
871    fn test_accumulator_rejects_oversized_input() {
872        let records = vec![
873            HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO };
874            MAX_BLOCKS_PER_ERE + 1
875        ];
876        assert!(Accumulator::from_header_records(&records).is_err());
877    }
878
879    #[test]
880    fn test_proof_type_byte_roundtrip() {
881        for ty in [
882            ProofType::BlockProofHistoricalHashesAccumulator,
883            ProofType::BlockProofHistoricalRoots,
884            ProofType::BlockProofHistoricalSummariesCapella,
885            ProofType::BlockProofHistoricalSummariesDeneb,
886        ] {
887            assert_eq!(ProofType::from_byte(ty.as_byte()), Some(ty));
888        }
889        assert_eq!(ProofType::from_byte(4), None);
890    }
891
892    #[test]
893    fn test_proof_roundtrip() {
894        let ssz_proof = vec![0xab; 64];
895        let proof = Proof::encode(ProofType::BlockProofHistoricalRoots, &ssz_proof).unwrap();
896
897        // Roundtrip through an Entry
898        let entry = proof.to_entry();
899        assert_eq!(entry.entry_type, PROOF);
900        let recovered = Proof::from_entry(&entry).unwrap();
901
902        let (proof_type, ssz_bytes) = recovered.decode().unwrap();
903        assert_eq!(proof_type, ProofType::BlockProofHistoricalRoots);
904        assert_eq!(ssz_bytes, ssz_proof);
905    }
906
907    #[test]
908    fn test_from_entry_rejects_wrong_type() {
909        let entry = Entry::new(COMPRESSED_BODY, vec![1, 2, 3]);
910        assert!(CompressedHeader::from_entry(&entry).is_err());
911    }
912
913    #[test]
914    fn test_decode_rejects_trailing_bytes() {
915        // A record is exactly `snappyFramed(rlp(...))`; an extra byte after the RLP value
916        // must be rejected, not silently ignored.
917        let mut rlp = Vec::new();
918        7u64.encode(&mut rlp);
919        rlp.push(0xff);
920        let compressed = CompressedHeader::from_rlp(&rlp).unwrap();
921        assert!(compressed.decode::<u64>().is_err());
922    }
923
924    #[test]
925    fn test_proof_decode_rejects_trailing_bytes() {
926        let valid = Proof::encode(ProofType::BlockProofHistoricalRoots, &[1, 2, 3]).unwrap();
927        let mut raw = snappy_decompress(&valid.data).unwrap();
928        raw.push(0xff); // byte beyond the RLP list
929        let tampered = Proof::new(snappy_compress(&raw).unwrap());
930        assert!(tampered.decode().is_err());
931    }
932
933    #[test]
934    fn test_proof_decode_rejects_extra_list_item() {
935        // Build rlp([proof-type, ssz, extra]) — a third item must be rejected.
936        let mut payload = Vec::new();
937        0u8.encode(&mut payload);
938        alloy_primitives::Bytes::from(vec![1, 2, 3]).encode(&mut payload);
939        99u8.encode(&mut payload);
940        let mut rlp = Vec::new();
941        alloy_rlp::Header { list: true, payload_length: payload.len() }.encode(&mut rlp);
942        rlp.extend_from_slice(&payload);
943        let proof = Proof::new(snappy_compress(&rlp).unwrap());
944        assert!(proof.decode().is_err());
945    }
946
947    #[test]
948    fn test_receipt_list_compression() {
949        let receipts = create_test_receipts();
950
951        // Compress the list of receipts
952        let compressed_receipts = CompressedSlimReceipts::from_encodable_list(&receipts)
953            .expect("Failed to compress receipt list");
954
955        // Decode the compressed receipts back. ERE always stores slim receipts (no bloom), so the
956        // bare `Receipt` (`alloy_consensus::EthereumReceipt`) is the canonical decode target.
957        let decoded_receipts: Vec<Receipt> =
958            compressed_receipts.decode().expect("Failed to decode compressed receipt list");
959
960        // Verify that the decoded receipts match the original
961        assert_eq!(decoded_receipts.len(), receipts.len());
962
963        for (original, decoded) in receipts.iter().zip(decoded_receipts.iter()) {
964            assert_eq!(decoded.tx_type, original.tx_type);
965            assert_eq!(decoded.success, original.success);
966            assert_eq!(decoded.cumulative_gas_used, original.cumulative_gas_used);
967            assert_eq!(decoded.logs.len(), original.logs.len());
968
969            for (original_log, decoded_log) in original.logs.iter().zip(decoded.logs.iter()) {
970                assert_eq!(decoded_log.address, original_log.address);
971                assert_eq!(decoded_log.data.topics(), original_log.data.topics());
972            }
973        }
974    }
975}