Skip to main content

reth_era/era1/types/
execution.rs

1//! Execution layer specific types for `.era1` files
2//!
3//! Contains implementations for compressed execution layer data structures:
4//! - [`CompressedHeader`] - Block header
5//! - [`CompressedBody`] - Block body
6//! - [`CompressedReceipts`] - 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/era1.md>
12//!
13//! # Examples
14//!
15//! ## [`CompressedHeader`]
16//!
17//! ```rust
18//! use alloy_consensus::Header;
19//! use reth_era::{common::decode::DecodeCompressedRlp, era1::types::execution::CompressedHeader};
20//!
21//! let header = Header { number: 100, ..Default::default() };
22//! // Compress the header: rlp encoding and Snappy compression
23//! let compressed = CompressedHeader::from_header(&header)?;
24//! // Decompressed and decode typed compressed header
25//! let decoded_header: Header = compressed.decode_header()?;
26//! assert_eq!(decoded_header.number, 100);
27//! # Ok::<(), reth_era::e2s::error::E2sError>(())
28//! ```
29//!
30//! ## [`CompressedBody`]
31//!
32//! ```rust
33//! use alloy_consensus::{BlockBody, Header};
34//! use alloy_primitives::Bytes;
35//! use reth_era::{common::decode::DecodeCompressedRlp, era1::types::execution::CompressedBody};
36//! use reth_ethereum_primitives::TransactionSigned;
37//!
38//! let body: BlockBody<Bytes> = BlockBody {
39//!     transactions: vec![Bytes::from(vec![1, 2, 3])],
40//!     ommers: vec![],
41//!     withdrawals: None,
42//! };
43//! // Compress the body: rlp encoding and snappy compression
44//! let compressed_body = CompressedBody::from_body(&body)?;
45//! // Decode back to typed body by decompressing and decoding
46//! let decoded_body: alloy_consensus::BlockBody<alloy_primitives::Bytes> =
47//!     compressed_body.decode()?;
48//! assert_eq!(decoded_body.transactions.len(), 1);
49//! # Ok::<(), reth_era::e2s::error::E2sError>(())
50//! ```
51//!
52//! ## [`CompressedReceipts`]
53//!
54//! ```rust
55//! use alloy_consensus::{Eip658Value, Receipt, ReceiptEnvelope, ReceiptWithBloom};
56//! use reth_era::{
57//!     common::decode::DecodeCompressedRlp, era1::types::execution::CompressedReceipts,
58//! };
59//!
60//! let receipt =
61//!     Receipt { status: Eip658Value::Eip658(true), cumulative_gas_used: 21000, logs: vec![] };
62//! let receipt_with_bloom = ReceiptWithBloom::new(receipt, Default::default());
63//! let enveloped_receipt = ReceiptEnvelope::Legacy(receipt_with_bloom);
64//! // Compress the receipt: rlp encoding and snappy compression
65//! let compressed_receipt_data = CompressedReceipts::from_encodable(&enveloped_receipt)?;
66//! // Get raw receipt by decoding and decompressing compressed and encoded receipt
67//! let decompressed_receipt = compressed_receipt_data.decode::<ReceiptEnvelope>()?;
68//! assert_eq!(decompressed_receipt.cumulative_gas_used(), 21000);
69//! # Ok::<(), reth_era::e2s::error::E2sError>(())
70//! ``````
71
72use crate::{
73    common::decode::DecodeCompressedRlp,
74    e2s::{error::E2sError, types::Entry},
75};
76use alloy_consensus::{Block, BlockBody, Header};
77use alloy_primitives::{B256, U256};
78use alloy_rlp::{Decodable, Encodable};
79use sha2::{Digest, Sha256};
80use snap::{read::FrameDecoder, write::FrameEncoder};
81use std::{
82    io::{Read, Write},
83    marker::PhantomData,
84};
85
86// Era1-specific constants
87/// `CompressedHeader` record type
88pub const COMPRESSED_HEADER: [u8; 2] = [0x03, 0x00];
89
90/// `CompressedBody` record type
91pub const COMPRESSED_BODY: [u8; 2] = [0x04, 0x00];
92
93/// `CompressedReceipts` record type
94pub const COMPRESSED_RECEIPTS: [u8; 2] = [0x05, 0x00];
95
96/// `TotalDifficulty` record type
97pub const TOTAL_DIFFICULTY: [u8; 2] = [0x06, 0x00];
98
99/// `Accumulator` record type
100pub const ACCUMULATOR: [u8; 2] = [0x07, 0x00];
101
102/// Maximum number of blocks in an Era1 file, limited by accumulator size
103pub const MAX_BLOCKS_PER_ERA1: usize = 8192;
104
105/// Generic codec for Snappy-compressed RLP data
106#[derive(Debug, Clone, Default)]
107pub struct SnappyRlpCodec<T> {
108    _phantom: PhantomData<T>,
109}
110
111impl<T> SnappyRlpCodec<T> {
112    /// Create a new codec for the given type
113    pub const fn new() -> Self {
114        Self { _phantom: PhantomData }
115    }
116}
117
118impl<T: Decodable> SnappyRlpCodec<T> {
119    /// Decode compressed data into the target type
120    pub fn decode(&self, compressed_data: &[u8]) -> Result<T, E2sError> {
121        let mut decoder = FrameDecoder::new(compressed_data);
122        let mut decompressed = Vec::new();
123        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
124            E2sError::SnappyDecompression(format!("Failed to decompress data: {e}"))
125        })?;
126
127        let mut slice = decompressed.as_slice();
128        T::decode(&mut slice).map_err(|e| E2sError::Rlp(format!("Failed to decode RLP data: {e}")))
129    }
130}
131
132impl<T: Encodable> SnappyRlpCodec<T> {
133    /// Encode data into compressed format
134    pub fn encode(&self, data: &T) -> Result<Vec<u8>, E2sError> {
135        let mut rlp_data = Vec::new();
136        data.encode(&mut rlp_data);
137
138        let mut compressed = Vec::new();
139        {
140            let mut encoder = FrameEncoder::new(&mut compressed);
141
142            Write::write_all(&mut encoder, &rlp_data).map_err(|e| {
143                E2sError::SnappyCompression(format!("Failed to compress data: {e}"))
144            })?;
145
146            encoder.flush().map_err(|e| {
147                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
148            })?;
149        }
150
151        Ok(compressed)
152    }
153}
154
155/// Compressed block header using `snappyFramed(rlp(header))`
156#[derive(Debug, Clone)]
157pub struct CompressedHeader {
158    /// The compressed data
159    pub data: Vec<u8>,
160}
161
162impl CompressedHeader {
163    /// Create a new [`CompressedHeader`] from compressed data
164    pub const fn new(data: Vec<u8>) -> Self {
165        Self { data }
166    }
167
168    /// Create from RLP-encoded header by compressing it with Snappy
169    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
170        let mut compressed = Vec::new();
171        {
172            let mut encoder = FrameEncoder::new(&mut compressed);
173
174            Write::write_all(&mut encoder, rlp_data).map_err(|e| {
175                E2sError::SnappyCompression(format!("Failed to compress header: {e}"))
176            })?;
177
178            encoder.flush().map_err(|e| {
179                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
180            })?;
181        }
182        Ok(Self { data: compressed })
183    }
184
185    /// Decompress to get the original RLP-encoded header
186    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
187        let mut decoder = FrameDecoder::new(self.data.as_slice());
188        let mut decompressed = Vec::new();
189        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
190            E2sError::SnappyDecompression(format!("Failed to decompress header: {e}"))
191        })?;
192
193        Ok(decompressed)
194    }
195
196    /// Convert to an [`Entry`]
197    pub fn to_entry(&self) -> Entry {
198        Entry::new(COMPRESSED_HEADER, self.data.clone())
199    }
200
201    /// Create from an [`Entry`]
202    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
203        if entry.entry_type != COMPRESSED_HEADER {
204            return Err(E2sError::Ssz(format!(
205                "Invalid entry type for CompressedHeader: expected {:02x}{:02x}, got {:02x}{:02x}",
206                COMPRESSED_HEADER[0],
207                COMPRESSED_HEADER[1],
208                entry.entry_type[0],
209                entry.entry_type[1]
210            )));
211        }
212
213        Ok(Self { data: entry.data.clone() })
214    }
215
216    /// Decode this compressed header into an `alloy_consensus::Header`
217    pub fn decode_header(&self) -> Result<Header, E2sError> {
218        self.decode()
219    }
220
221    /// Create a [`CompressedHeader`] from a header
222    pub fn from_header<H: Encodable>(header: &H) -> Result<Self, E2sError> {
223        let encoder = SnappyRlpCodec::new();
224        let compressed = encoder.encode(header)?;
225        Ok(Self::new(compressed))
226    }
227}
228
229impl DecodeCompressedRlp for CompressedHeader {
230    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
231        let decoder = SnappyRlpCodec::<T>::new();
232        decoder.decode(&self.data)
233    }
234}
235
236/// Compressed block body using `snappyFramed(rlp(body))`
237#[derive(Debug, Clone)]
238pub struct CompressedBody {
239    /// The compressed data
240    pub data: Vec<u8>,
241}
242
243impl CompressedBody {
244    /// Create a new [`CompressedBody`] from compressed data
245    pub const fn new(data: Vec<u8>) -> Self {
246        Self { data }
247    }
248
249    /// Create from RLP-encoded body by compressing it with Snappy
250    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
251        let mut compressed = Vec::new();
252        {
253            let mut encoder = FrameEncoder::new(&mut compressed);
254
255            Write::write_all(&mut encoder, rlp_data).map_err(|e| {
256                E2sError::SnappyCompression(format!("Failed to compress body: {e}"))
257            })?;
258
259            encoder.flush().map_err(|e| {
260                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
261            })?;
262        }
263        Ok(Self { data: compressed })
264    }
265
266    /// Decompress to get the original RLP-encoded body
267    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
268        let mut decoder = FrameDecoder::new(self.data.as_slice());
269        let mut decompressed = Vec::new();
270        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
271            E2sError::SnappyDecompression(format!("Failed to decompress body: {e}"))
272        })?;
273
274        Ok(decompressed)
275    }
276
277    /// Convert to an [`Entry`]
278    pub fn to_entry(&self) -> Entry {
279        Entry::new(COMPRESSED_BODY, self.data.clone())
280    }
281
282    /// Create from an [`Entry`]
283    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
284        if entry.entry_type != COMPRESSED_BODY {
285            return Err(E2sError::Ssz(format!(
286                "Invalid entry type for CompressedBody: expected {:02x}{:02x}, got {:02x}{:02x}",
287                COMPRESSED_BODY[0], COMPRESSED_BODY[1], entry.entry_type[0], entry.entry_type[1]
288            )));
289        }
290
291        Ok(Self { data: entry.data.clone() })
292    }
293
294    /// Decode this [`CompressedBody`] into an `alloy_consensus::BlockBody`
295    pub fn decode_body<T: Decodable, H: Decodable>(&self) -> Result<BlockBody<T, H>, E2sError> {
296        let decompressed = self.decompress()?;
297        Self::decode_body_from_decompressed(&decompressed)
298    }
299
300    /// Decode decompressed body data into an `alloy_consensus::BlockBody`
301    pub fn decode_body_from_decompressed<T: Decodable, H: Decodable>(
302        data: &[u8],
303    ) -> Result<BlockBody<T, H>, E2sError> {
304        alloy_rlp::decode_exact::<BlockBody<T, H>>(data)
305            .map_err(|e| E2sError::Rlp(format!("Failed to decode RLP data: {e}")))
306    }
307
308    /// Create a [`CompressedBody`] from a block body (e.g.  `alloy_consensus::BlockBody`)
309    pub fn from_body<B: Encodable>(body: &B) -> Result<Self, E2sError> {
310        let encoder = SnappyRlpCodec::new();
311        let compressed = encoder.encode(body)?;
312        Ok(Self::new(compressed))
313    }
314}
315
316impl DecodeCompressedRlp for CompressedBody {
317    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
318        let decoder = SnappyRlpCodec::<T>::new();
319        decoder.decode(&self.data)
320    }
321}
322
323/// Compressed receipts using snappyFramed(rlp(receipts))
324#[derive(Debug, Clone)]
325pub struct CompressedReceipts {
326    /// The compressed data
327    pub data: Vec<u8>,
328}
329
330impl CompressedReceipts {
331    /// Create a new [`CompressedReceipts`] from compressed data
332    pub const fn new(data: Vec<u8>) -> Self {
333        Self { data }
334    }
335
336    /// Create from RLP-encoded receipts by compressing it with Snappy
337    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
338        let mut compressed = Vec::new();
339        {
340            let mut encoder = FrameEncoder::new(&mut compressed);
341
342            Write::write_all(&mut encoder, rlp_data).map_err(|e| {
343                E2sError::SnappyCompression(format!("Failed to compress receipts: {e}"))
344            })?;
345
346            encoder.flush().map_err(|e| {
347                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
348            })?;
349        }
350        Ok(Self { data: compressed })
351    }
352    /// Decompress to get the original RLP-encoded receipts
353    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
354        let mut decoder = FrameDecoder::new(self.data.as_slice());
355        let mut decompressed = Vec::new();
356        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
357            E2sError::SnappyDecompression(format!("Failed to decompress receipts: {e}"))
358        })?;
359
360        Ok(decompressed)
361    }
362
363    /// Convert to an [`Entry`]
364    pub fn to_entry(&self) -> Entry {
365        Entry::new(COMPRESSED_RECEIPTS, self.data.clone())
366    }
367
368    /// Create from an [`Entry`]
369    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
370        if entry.entry_type != COMPRESSED_RECEIPTS {
371            return Err(E2sError::Ssz(format!(
372                "Invalid entry type for CompressedReceipts: expected {:02x}{:02x}, got {:02x}{:02x}",
373                COMPRESSED_RECEIPTS[0], COMPRESSED_RECEIPTS[1],
374                entry.entry_type[0], entry.entry_type[1]
375            )));
376        }
377
378        Ok(Self { data: entry.data.clone() })
379    }
380
381    /// Decode this [`CompressedReceipts`] into the given type
382    pub fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
383        let decoder = SnappyRlpCodec::<T>::new();
384        decoder.decode(&self.data)
385    }
386
387    /// Create [`CompressedReceipts`] from an encodable type
388    pub fn from_encodable<T: Encodable>(data: &T) -> Result<Self, E2sError> {
389        let encoder = SnappyRlpCodec::<T>::new();
390        let compressed = encoder.encode(data)?;
391        Ok(Self::new(compressed))
392    }
393    /// Encode a list of receipts to RLP format
394    pub fn encode_receipts_to_rlp<T: Encodable>(receipts: &[T]) -> Result<Vec<u8>, E2sError> {
395        let mut rlp_data = Vec::new();
396        alloy_rlp::encode_list(receipts, &mut rlp_data);
397        Ok(rlp_data)
398    }
399
400    /// Encode and compress a list of receipts
401    pub fn from_encodable_list<T: Encodable>(receipts: &[T]) -> Result<Self, E2sError> {
402        let rlp_data = Self::encode_receipts_to_rlp(receipts)?;
403        Self::from_rlp(&rlp_data)
404    }
405}
406
407impl DecodeCompressedRlp for CompressedReceipts {
408    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
409        let decoder = SnappyRlpCodec::<T>::new();
410        decoder.decode(&self.data)
411    }
412}
413
414/// Total difficulty for a block
415#[derive(Debug, Clone)]
416pub struct TotalDifficulty {
417    /// The total difficulty as U256
418    pub value: U256,
419}
420
421impl TotalDifficulty {
422    /// Create a new [`TotalDifficulty`] from a U256 value
423    pub const fn new(value: U256) -> Self {
424        Self { value }
425    }
426
427    /// Convert to an [`Entry`]
428    pub fn to_entry(&self) -> Entry {
429        // era1 spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
430        let data = self.value.to_le_bytes::<32>().to_vec();
431        Entry::new(TOTAL_DIFFICULTY, data)
432    }
433
434    /// Create from an [`Entry`]
435    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
436        if entry.entry_type != TOTAL_DIFFICULTY {
437            return Err(E2sError::Ssz(format!(
438                "Invalid entry type for TotalDifficulty: expected {:02x}{:02x}, got {:02x}{:02x}",
439                TOTAL_DIFFICULTY[0], TOTAL_DIFFICULTY[1], entry.entry_type[0], entry.entry_type[1]
440            )));
441        }
442
443        if entry.data.len() != 32 {
444            return Err(E2sError::Ssz(format!(
445                "Invalid data length for TotalDifficulty: expected 32, got {}",
446                entry.data.len()
447            )));
448        }
449
450        // era1 spec: `total-difficulty = { type: 0x0600, data: SSZ uint256 }` (little-endian)
451        let value = U256::from_le_slice(&entry.data);
452
453        Ok(Self { value })
454    }
455}
456
457/// Accumulator is computed by constructing an SSZ list of header-records
458/// and calculating the `hash_tree_root`
459#[derive(Debug, Clone)]
460pub struct Accumulator {
461    /// The accumulator root hash
462    pub root: B256,
463}
464
465impl Accumulator {
466    /// Create a new [`Accumulator`] from a root hash
467    pub const fn new(root: B256) -> Self {
468        Self { root }
469    }
470
471    /// Convert to an [`Entry`]
472    pub fn to_entry(&self) -> Entry {
473        Entry::new(ACCUMULATOR, self.root.to_vec())
474    }
475
476    /// Create from an [`Entry`]
477    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
478        if entry.entry_type != ACCUMULATOR {
479            return Err(E2sError::Ssz(format!(
480                "Invalid entry type for Accumulator: expected {:02x}{:02x}, got {:02x}{:02x}",
481                ACCUMULATOR[0], ACCUMULATOR[1], entry.entry_type[0], entry.entry_type[1]
482            )));
483        }
484
485        if entry.data.len() != 32 {
486            return Err(E2sError::Ssz(format!(
487                "Invalid data length for Accumulator: expected 32, got {}",
488                entry.data.len()
489            )));
490        }
491
492        let mut root = [0u8; 32];
493        root.copy_from_slice(&entry.data);
494
495        Ok(Self { root: B256::from(root) })
496    }
497
498    /// Compute the accumulator from a list of header records.
499    ///
500    /// Implements `hash_tree_root(List[HeaderRecord, 8192])` per the ERA1 spec:
501    /// - Each leaf is `sha256(block_hash || total_difficulty_le_bytes32)`
502    /// - Leaves are padded to `MAX_BLOCKS_PER_ERA1` (8192) with zero hashes
503    /// - Binary Merkle tree is computed bottom-up
504    /// - Final root is `sha256(merkle_root || le_bytes32(actual_count))`
505    ///
506    /// Returns `Err` if `records` exceeds [`MAX_BLOCKS_PER_ERA1`].
507    pub fn from_header_records(records: &[HeaderRecord]) -> Result<Self, E2sError> {
508        let capacity = MAX_BLOCKS_PER_ERA1;
509
510        if records.len() > capacity {
511            return Err(E2sError::Ssz(format!(
512                "Too many header records: got {}, max {}",
513                records.len(),
514                capacity
515            )));
516        }
517
518        // Compute leaf hash for each header record
519        let mut leaves = Vec::with_capacity(capacity);
520        for record in records {
521            let mut data = [0u8; 64];
522            data[..32].copy_from_slice(record.block_hash.as_slice());
523            data[32..].copy_from_slice(&record.total_difficulty.to_le_bytes::<32>());
524            leaves.push(<[u8; 32]>::from(Sha256::digest(data)));
525        }
526
527        // Pad to capacity with zero hashes
528        leaves.resize(capacity, [0u8; 32]);
529
530        // Binary Merkle tree bottom-up (capacity is always a power of two)
531        while leaves.len() > 1 {
532            let mut next_level = Vec::with_capacity(leaves.len() / 2);
533            for pair in leaves.chunks_exact(2) {
534                let mut data = [0u8; 64];
535                data[..32].copy_from_slice(&pair[0]);
536                data[32..].copy_from_slice(&pair[1]);
537                next_level.push(<[u8; 32]>::from(Sha256::digest(data)));
538            }
539            leaves = next_level;
540        }
541
542        let merkle_root = leaves[0];
543
544        // mix_in_length: sha256(merkle_root || le_bytes32(actual_length))
545        let mut mix = [0u8; 64];
546        mix[..32].copy_from_slice(&merkle_root);
547        let length = records.len() as u64;
548        mix[32..40].copy_from_slice(&length.to_le_bytes());
549        // remaining bytes stay zero (uint256 LE padding)
550
551        Ok(Self { root: B256::from(<[u8; 32]>::from(Sha256::digest(mix))) })
552    }
553}
554
555/// A header record used to compute the ERA1 accumulator.
556///
557/// Per the ERA1 spec: `header-record := { block-hash: Bytes32, total-difficulty: Uint256 }`
558#[derive(Debug, Clone)]
559pub struct HeaderRecord {
560    /// The block hash (keccak256 of RLP-encoded header)
561    pub block_hash: B256,
562    /// The cumulative total difficulty at this block
563    pub total_difficulty: U256,
564}
565
566/// A block tuple in an Era1 file, containing all components for a single block
567#[derive(Debug, Clone)]
568pub struct BlockTuple {
569    /// Compressed block header
570    pub header: CompressedHeader,
571
572    /// Compressed block body
573    pub body: CompressedBody,
574
575    /// Compressed receipts
576    pub receipts: CompressedReceipts,
577
578    /// Total difficulty
579    pub total_difficulty: TotalDifficulty,
580}
581
582impl BlockTuple {
583    /// Create a new [`BlockTuple`]
584    pub const fn new(
585        header: CompressedHeader,
586        body: CompressedBody,
587        receipts: CompressedReceipts,
588        total_difficulty: TotalDifficulty,
589    ) -> Self {
590        Self { header, body, receipts, total_difficulty }
591    }
592
593    /// Convert to an `alloy_consensus::Block`
594    pub fn to_alloy_block<T: Decodable>(&self) -> Result<Block<T>, E2sError> {
595        let header: Header = self.header.decode()?;
596        let body: BlockBody<T> = self.body.decode()?;
597
598        Ok(Block::new(header, body))
599    }
600
601    /// Create from an `alloy_consensus::Block`
602    pub fn from_alloy_block<T: Encodable, R: Encodable>(
603        block: &Block<T>,
604        receipts: &R,
605        total_difficulty: U256,
606    ) -> Result<Self, E2sError> {
607        let header = CompressedHeader::from_header(&block.header)?;
608        let body = CompressedBody::from_body(&block.body)?;
609
610        let compressed_receipts = CompressedReceipts::from_encodable(receipts)?;
611
612        let difficulty = TotalDifficulty::new(total_difficulty);
613
614        Ok(Self::new(header, body, compressed_receipts, difficulty))
615    }
616}
617
618#[cfg(test)]
619mod tests {
620    use super::*;
621    use crate::test_utils::{create_header, create_test_receipt, create_test_receipts};
622    use alloy_eips::eip4895::Withdrawals;
623    use alloy_primitives::{Bytes, U256};
624    use reth_ethereum_primitives::{Receipt, TxType};
625
626    #[test]
627    fn test_header_conversion_roundtrip() {
628        let header = create_header();
629
630        let compressed_header = CompressedHeader::from_header(&header).unwrap();
631
632        let decoded_header = compressed_header.decode_header().unwrap();
633
634        assert_eq!(header.number, decoded_header.number);
635        assert_eq!(header.difficulty, decoded_header.difficulty);
636        assert_eq!(header.timestamp, decoded_header.timestamp);
637        assert_eq!(header.gas_used, decoded_header.gas_used);
638        assert_eq!(header.parent_hash, decoded_header.parent_hash);
639        assert_eq!(header.base_fee_per_gas, decoded_header.base_fee_per_gas);
640    }
641
642    #[test]
643    fn test_block_body_conversion() {
644        let block_body: BlockBody<Bytes> =
645            BlockBody { transactions: vec![], ommers: vec![], withdrawals: None };
646
647        let compressed_body = CompressedBody::from_body(&block_body).unwrap();
648
649        let decoded_body: BlockBody<Bytes> = compressed_body.decode_body().unwrap();
650
651        assert_eq!(decoded_body.transactions.len(), 0);
652        assert_eq!(decoded_body.ommers.len(), 0);
653        assert_eq!(decoded_body.withdrawals, None);
654    }
655
656    #[test]
657    fn test_total_difficulty_roundtrip() {
658        let value = U256::from(123456789u64);
659
660        let total_difficulty = TotalDifficulty::new(value);
661
662        let entry = total_difficulty.to_entry();
663
664        assert_eq!(entry.entry_type, TOTAL_DIFFICULTY);
665
666        let recovered = TotalDifficulty::from_entry(&entry).unwrap();
667
668        assert_eq!(recovered.value, value);
669    }
670
671    #[test]
672    fn test_total_difficulty_ssz_le_encoding() {
673        // Verify that total-difficulty is encoded as SSZ uint256 (little-endian).
674        // See https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era1.md
675        let value = U256::from(1u64);
676        let td = TotalDifficulty::new(value);
677        let entry = td.to_entry();
678
679        // Little-endian: least significant byte first [1, 0, 0, ..., 0]
680        assert_eq!(entry.data[0], 1, "First byte must be 1 (little-endian)");
681        assert_eq!(entry.data[31], 0, "Last byte must be 0 (little-endian)");
682    }
683
684    #[test]
685    fn test_compression_roundtrip() {
686        let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
687
688        // Test header compression/decompression
689        let compressed_header = CompressedHeader::from_rlp(&rlp_data).unwrap();
690        let decompressed = compressed_header.decompress().unwrap();
691        assert_eq!(decompressed, rlp_data);
692
693        // Test body compression/decompression
694        let compressed_body = CompressedBody::from_rlp(&rlp_data).unwrap();
695        let decompressed = compressed_body.decompress().unwrap();
696        assert_eq!(decompressed, rlp_data);
697
698        // Test receipts compression/decompression
699        let compressed_receipts = CompressedReceipts::from_rlp(&rlp_data).unwrap();
700        let decompressed = compressed_receipts.decompress().unwrap();
701        assert_eq!(decompressed, rlp_data);
702    }
703
704    #[test]
705    fn test_block_tuple_with_data() {
706        // Create block with transactions and withdrawals
707        let header = create_header();
708
709        let transactions = vec![Bytes::from(vec![1, 2, 3, 4]), Bytes::from(vec![5, 6, 7, 8])];
710
711        let withdrawals = Some(Withdrawals(vec![]));
712
713        let block_body = BlockBody { transactions, ommers: vec![], withdrawals };
714
715        let block = Block::new(header, block_body);
716
717        let receipts: Vec<u8> = Vec::new();
718
719        let block_tuple =
720            BlockTuple::from_alloy_block(&block, &receipts, U256::from(123456u64)).unwrap();
721
722        // Convert back to Block
723        let decoded_block: Block<Bytes> = block_tuple.to_alloy_block().unwrap();
724
725        // Verify block components
726        assert_eq!(decoded_block.header.number, 100);
727        assert_eq!(decoded_block.body.transactions.len(), 2);
728        assert_eq!(decoded_block.body.transactions[0], Bytes::from(vec![1, 2, 3, 4]));
729        assert_eq!(decoded_block.body.transactions[1], Bytes::from(vec![5, 6, 7, 8]));
730        assert!(decoded_block.body.withdrawals.is_some());
731    }
732
733    #[test]
734    fn test_single_receipt_compression_roundtrip() {
735        let test_receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
736
737        // Compress the receipt
738        let compressed_receipts =
739            CompressedReceipts::from_encodable(&test_receipt).expect("Failed to compress receipt");
740
741        // Verify compression
742        assert!(!compressed_receipts.data.is_empty());
743
744        // Decode the compressed receipt back
745        let decoded_receipt: Receipt =
746            compressed_receipts.decode().expect("Failed to decode compressed receipt");
747
748        // Verify that the decoded receipt matches the original
749        assert_eq!(decoded_receipt.tx_type, test_receipt.tx_type);
750        assert_eq!(decoded_receipt.success, test_receipt.success);
751        assert_eq!(decoded_receipt.cumulative_gas_used, test_receipt.cumulative_gas_used);
752        assert_eq!(decoded_receipt.logs.len(), test_receipt.logs.len());
753
754        // Verify each log
755        for (original_log, decoded_log) in test_receipt.logs.iter().zip(decoded_receipt.logs.iter())
756        {
757            assert_eq!(decoded_log.address, original_log.address);
758            assert_eq!(decoded_log.data.topics(), original_log.data.topics());
759        }
760    }
761
762    #[test]
763    fn test_accumulator_from_header_records_known_vectors() {
764        // Known-answer vectors computed from the SSZ spec:
765        //   hash_tree_root(List[HeaderRecord, 8192])
766        let expected_empty: B256 =
767            "4a8c3a07c8d23adc5bac61157555c3c784d53d9bc110c1370809bd23cd93777d".parse().unwrap();
768        let expected_single_zero: B256 =
769            "81fd641249670887a731386e756a7a1538dc781b1b0bf016889045d350812817".parse().unwrap();
770        let expected_single_nonzero: B256 =
771            "ada35c48d81117f4fd588554cd4c4752356336e84cb41106dea1ceb4cfac8799".parse().unwrap();
772
773        // Empty list
774        let acc_empty = Accumulator::from_header_records(&[]).unwrap();
775        assert_eq!(acc_empty.root, expected_empty);
776
777        // Single record with zero values
778        let records = vec![HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO }];
779        let acc = Accumulator::from_header_records(&records).unwrap();
780        assert_eq!(acc.root, expected_single_zero);
781
782        // Single record with non-zero values
783        let records2 = vec![HeaderRecord {
784            block_hash: B256::from([1u8; 32]),
785            total_difficulty: U256::from(100u64),
786        }];
787        let acc2 = Accumulator::from_header_records(&records2).unwrap();
788        assert_eq!(acc2.root, expected_single_nonzero);
789    }
790
791    #[test]
792    fn test_accumulator_rejects_oversized_input() {
793        let records = vec![
794            HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO };
795            MAX_BLOCKS_PER_ERA1 + 1
796        ];
797        assert!(Accumulator::from_header_records(&records).is_err());
798    }
799
800    #[test]
801    fn test_receipt_list_compression() {
802        let receipts = create_test_receipts();
803
804        // Compress the list of receipts
805        let compressed_receipts = CompressedReceipts::from_encodable_list(&receipts)
806            .expect("Failed to compress receipt list");
807
808        // Decode the compressed receipts back
809        // Note: For real ERA1 files, use `Vec<ReceiptWithBloom>` before Era ~1520 or use
810        // `Vec<ReceiptEnvelope>` after this era
811        let decoded_receipts: Vec<Receipt> =
812            compressed_receipts.decode().expect("Failed to decode compressed receipt list");
813
814        // Verify that the decoded receipts match the original
815        assert_eq!(decoded_receipts.len(), receipts.len());
816
817        for (original, decoded) in receipts.iter().zip(decoded_receipts.iter()) {
818            assert_eq!(decoded.tx_type, original.tx_type);
819            assert_eq!(decoded.success, original.success);
820            assert_eq!(decoded.cumulative_gas_used, original.cumulative_gas_used);
821            assert_eq!(decoded.logs.len(), original.logs.len());
822
823            for (original_log, decoded_log) in original.logs.iter().zip(decoded.logs.iter()) {
824                assert_eq!(decoded_log.address, original_log.address);
825                assert_eq!(decoded_log.data.topics(), original_log.data.topics());
826            }
827        }
828    }
829}