reth_era/
execution_types.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::{execution_types::CompressedHeader, DecodeCompressed};
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_types::E2sError>(())
28//! ```
29//!
30//! ## [`CompressedBody`]
31//!
32//! ```rust
33//! use alloy_consensus::{BlockBody, Header};
34//! use alloy_primitives::Bytes;
35//! use reth_era::{execution_types::CompressedBody, DecodeCompressed};
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_types::E2sError>(())
50//! ```
51//!
52//! ## [`CompressedReceipts`]
53//!
54//! ```rust
55//! use alloy_consensus::ReceiptWithBloom;
56//! use reth_era::{execution_types::CompressedReceipts, DecodeCompressed};
57//! use reth_ethereum_primitives::{Receipt, TxType};
58//!
59//! let receipt = Receipt {
60//!     tx_type: TxType::Legacy,
61//!     success: true,
62//!     cumulative_gas_used: 21000,
63//!     logs: vec![],
64//! };
65//! let receipt_with_bloom = ReceiptWithBloom { receipt, logs_bloom: Default::default() };
66//! // Compress the receipt: rlp encoding and snappy compression
67//! let compressed_receipt_data = CompressedReceipts::from_encodable(&receipt_with_bloom)?;
68//! // Get raw receipt by decoding and decompressing compressed and encoded receipt
69//! let decompressed_receipt = compressed_receipt_data.decode::<ReceiptWithBloom>()?;
70//! assert_eq!(decompressed_receipt.receipt.cumulative_gas_used, 21000);
71//! # Ok::<(), reth_era::e2s_types::E2sError>(())
72//! ``````
73
74use crate::{
75    e2s_types::{E2sError, Entry},
76    DecodeCompressed,
77};
78use alloy_consensus::{Block, BlockBody, Header};
79use alloy_primitives::{B256, U256};
80use alloy_rlp::{Decodable, Encodable};
81use snap::{read::FrameDecoder, write::FrameEncoder};
82use std::{
83    io::{Read, Write},
84    marker::PhantomData,
85};
86
87// Era1-specific constants
88/// `CompressedHeader` record type
89pub const COMPRESSED_HEADER: [u8; 2] = [0x03, 0x00];
90
91/// `CompressedBody` record type
92pub const COMPRESSED_BODY: [u8; 2] = [0x04, 0x00];
93
94/// `CompressedReceipts` record type
95pub const COMPRESSED_RECEIPTS: [u8; 2] = [0x05, 0x00];
96
97/// `TotalDifficulty` record type
98pub const TOTAL_DIFFICULTY: [u8; 2] = [0x06, 0x00];
99
100/// `Accumulator` record type
101pub const ACCUMULATOR: [u8; 2] = [0x07, 0x00];
102
103/// Maximum number of blocks in an Era1 file, limited by accumulator size
104pub const MAX_BLOCKS_PER_ERA1: usize = 8192;
105
106/// Generic codec for Snappy-compressed RLP data
107#[derive(Debug, Clone, Default)]
108pub struct SnappyRlpCodec<T> {
109    _phantom: PhantomData<T>,
110}
111
112impl<T> SnappyRlpCodec<T> {
113    /// Create a new codec for the given type
114    pub const fn new() -> Self {
115        Self { _phantom: PhantomData }
116    }
117}
118
119impl<T: Decodable> SnappyRlpCodec<T> {
120    /// Decode compressed data into the target type
121    pub fn decode(&self, compressed_data: &[u8]) -> Result<T, E2sError> {
122        let mut decoder = FrameDecoder::new(compressed_data);
123        let mut decompressed = Vec::new();
124        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
125            E2sError::SnappyDecompression(format!("Failed to decompress data: {e}"))
126        })?;
127
128        let mut slice = decompressed.as_slice();
129        T::decode(&mut slice).map_err(|e| E2sError::Rlp(format!("Failed to decode RLP data: {e}")))
130    }
131}
132
133impl<T: Encodable> SnappyRlpCodec<T> {
134    /// Encode data into compressed format
135    pub fn encode(&self, data: &T) -> Result<Vec<u8>, E2sError> {
136        let mut rlp_data = Vec::new();
137        data.encode(&mut rlp_data);
138
139        let mut compressed = Vec::new();
140        {
141            let mut encoder = FrameEncoder::new(&mut compressed);
142
143            Write::write_all(&mut encoder, &rlp_data).map_err(|e| {
144                E2sError::SnappyCompression(format!("Failed to compress data: {e}"))
145            })?;
146
147            encoder.flush().map_err(|e| {
148                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
149            })?;
150        }
151
152        Ok(compressed)
153    }
154}
155
156/// Compressed block header using `snappyFramed(rlp(header))`
157#[derive(Debug, Clone)]
158pub struct CompressedHeader {
159    /// The compressed data
160    pub data: Vec<u8>,
161}
162
163impl CompressedHeader {
164    /// Create a new [`CompressedHeader`] from compressed data
165    pub const fn new(data: Vec<u8>) -> Self {
166        Self { data }
167    }
168
169    /// Create from RLP-encoded header by compressing it with Snappy
170    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
171        let mut compressed = Vec::new();
172        {
173            let mut encoder = FrameEncoder::new(&mut compressed);
174
175            Write::write_all(&mut encoder, rlp_data).map_err(|e| {
176                E2sError::SnappyCompression(format!("Failed to compress header: {e}"))
177            })?;
178
179            encoder.flush().map_err(|e| {
180                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
181            })?;
182        }
183        Ok(Self { data: compressed })
184    }
185
186    /// Decompress to get the original RLP-encoded header
187    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
188        let mut decoder = FrameDecoder::new(self.data.as_slice());
189        let mut decompressed = Vec::new();
190        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
191            E2sError::SnappyDecompression(format!("Failed to decompress header: {e}"))
192        })?;
193
194        Ok(decompressed)
195    }
196
197    /// Convert to an [`Entry`]
198    pub fn to_entry(&self) -> Entry {
199        Entry::new(COMPRESSED_HEADER, self.data.clone())
200    }
201
202    /// Create from an [`Entry`]
203    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
204        if entry.entry_type != COMPRESSED_HEADER {
205            return Err(E2sError::Ssz(format!(
206                "Invalid entry type for CompressedHeader: expected {:02x}{:02x}, got {:02x}{:02x}",
207                COMPRESSED_HEADER[0],
208                COMPRESSED_HEADER[1],
209                entry.entry_type[0],
210                entry.entry_type[1]
211            )));
212        }
213
214        Ok(Self { data: entry.data.clone() })
215    }
216
217    /// Decode this compressed header into an `alloy_consensus::Header`
218    pub fn decode_header(&self) -> Result<Header, E2sError> {
219        self.decode()
220    }
221
222    /// Create a [`CompressedHeader`] from a header
223    pub fn from_header<H: Encodable>(header: &H) -> Result<Self, E2sError> {
224        let encoder = SnappyRlpCodec::new();
225        let compressed = encoder.encode(header)?;
226        Ok(Self::new(compressed))
227    }
228}
229
230impl DecodeCompressed for CompressedHeader {
231    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
232        let decoder = SnappyRlpCodec::<T>::new();
233        decoder.decode(&self.data)
234    }
235}
236
237/// Compressed block body using `snappyFramed(rlp(body))`
238#[derive(Debug, Clone)]
239pub struct CompressedBody {
240    /// The compressed data
241    pub data: Vec<u8>,
242}
243
244impl CompressedBody {
245    /// Create a new [`CompressedBody`] from compressed data
246    pub const fn new(data: Vec<u8>) -> Self {
247        Self { data }
248    }
249
250    /// Create from RLP-encoded body by compressing it with Snappy
251    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
252        let mut compressed = Vec::new();
253        {
254            let mut encoder = FrameEncoder::new(&mut compressed);
255
256            Write::write_all(&mut encoder, rlp_data).map_err(|e| {
257                E2sError::SnappyCompression(format!("Failed to compress header: {e}"))
258            })?;
259
260            encoder.flush().map_err(|e| {
261                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
262            })?;
263        }
264        Ok(Self { data: compressed })
265    }
266
267    /// Decompress to get the original RLP-encoded body
268    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
269        let mut decoder = FrameDecoder::new(self.data.as_slice());
270        let mut decompressed = Vec::new();
271        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
272            E2sError::SnappyDecompression(format!("Failed to decompress body: {e}"))
273        })?;
274
275        Ok(decompressed)
276    }
277
278    /// Convert to an [`Entry`]
279    pub fn to_entry(&self) -> Entry {
280        Entry::new(COMPRESSED_BODY, self.data.clone())
281    }
282
283    /// Create from an [`Entry`]
284    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
285        if entry.entry_type != COMPRESSED_BODY {
286            return Err(E2sError::Ssz(format!(
287                "Invalid entry type for CompressedBody: expected {:02x}{:02x}, got {:02x}{:02x}",
288                COMPRESSED_BODY[0], COMPRESSED_BODY[1], entry.entry_type[0], entry.entry_type[1]
289            )));
290        }
291
292        Ok(Self { data: entry.data.clone() })
293    }
294
295    /// Decode this [`CompressedBody`] into an `alloy_consensus::BlockBody`
296    pub fn decode_body<T: Decodable, H: Decodable>(&self) -> Result<BlockBody<T, H>, E2sError> {
297        let decompressed = self.decompress()?;
298        Self::decode_body_from_decompressed(&decompressed)
299    }
300
301    /// Decode decompressed body data into an `alloy_consensus::BlockBody`
302    pub fn decode_body_from_decompressed<T: Decodable, H: Decodable>(
303        data: &[u8],
304    ) -> Result<BlockBody<T, H>, E2sError> {
305        alloy_rlp::decode_exact::<BlockBody<T, H>>(data)
306            .map_err(|e| E2sError::Rlp(format!("Failed to decode RLP data: {e}")))
307    }
308
309    /// Create a [`CompressedBody`] from a block body (e.g.  `alloy_consensus::BlockBody`)
310    pub fn from_body<B: Encodable>(body: &B) -> Result<Self, E2sError> {
311        let encoder = SnappyRlpCodec::new();
312        let compressed = encoder.encode(body)?;
313        Ok(Self::new(compressed))
314    }
315}
316
317impl DecodeCompressed for CompressedBody {
318    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
319        let decoder = SnappyRlpCodec::<T>::new();
320        decoder.decode(&self.data)
321    }
322}
323
324/// Compressed receipts using snappyFramed(rlp(receipts))
325#[derive(Debug, Clone)]
326pub struct CompressedReceipts {
327    /// The compressed data
328    pub data: Vec<u8>,
329}
330
331impl CompressedReceipts {
332    /// Create a new [`CompressedReceipts`] from compressed data
333    pub const fn new(data: Vec<u8>) -> Self {
334        Self { data }
335    }
336
337    /// Create from RLP-encoded receipts by compressing it with Snappy
338    pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
339        let mut compressed = Vec::new();
340        {
341            let mut encoder = FrameEncoder::new(&mut compressed);
342
343            Write::write_all(&mut encoder, rlp_data).map_err(|e| {
344                E2sError::SnappyCompression(format!("Failed to compress header: {e}"))
345            })?;
346
347            encoder.flush().map_err(|e| {
348                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
349            })?;
350        }
351        Ok(Self { data: compressed })
352    }
353    /// Decompress to get the original RLP-encoded receipts
354    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
355        let mut decoder = FrameDecoder::new(self.data.as_slice());
356        let mut decompressed = Vec::new();
357        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
358            E2sError::SnappyDecompression(format!("Failed to decompress receipts: {e}"))
359        })?;
360
361        Ok(decompressed)
362    }
363
364    /// Convert to an [`Entry`]
365    pub fn to_entry(&self) -> Entry {
366        Entry::new(COMPRESSED_RECEIPTS, self.data.clone())
367    }
368
369    /// Create from an [`Entry`]
370    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
371        if entry.entry_type != COMPRESSED_RECEIPTS {
372            return Err(E2sError::Ssz(format!(
373                "Invalid entry type for CompressedReceipts: expected {:02x}{:02x}, got {:02x}{:02x}",
374                COMPRESSED_RECEIPTS[0], COMPRESSED_RECEIPTS[1],
375                entry.entry_type[0], entry.entry_type[1]
376            )));
377        }
378
379        Ok(Self { data: entry.data.clone() })
380    }
381
382    /// Decode this [`CompressedReceipts`] into the given type
383    pub fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
384        let decoder = SnappyRlpCodec::<T>::new();
385        decoder.decode(&self.data)
386    }
387
388    /// Create [`CompressedReceipts`] from an encodable type
389    pub fn from_encodable<T: Encodable>(data: &T) -> Result<Self, E2sError> {
390        let encoder = SnappyRlpCodec::<T>::new();
391        let compressed = encoder.encode(data)?;
392        Ok(Self::new(compressed))
393    }
394    /// Encode a list of receipts to RLP format
395    pub fn encode_receipts_to_rlp<T: Encodable>(receipts: &[T]) -> Result<Vec<u8>, E2sError> {
396        let mut rlp_data = Vec::new();
397        alloy_rlp::encode_list(receipts, &mut rlp_data);
398        Ok(rlp_data)
399    }
400
401    /// Encode and compress a list of receipts
402    pub fn from_encodable_list<T: Encodable>(receipts: &[T]) -> Result<Self, E2sError> {
403        let rlp_data = Self::encode_receipts_to_rlp(receipts)?;
404        Self::from_rlp(&rlp_data)
405    }
406}
407
408impl DecodeCompressed for CompressedReceipts {
409    fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
410        let decoder = SnappyRlpCodec::<T>::new();
411        decoder.decode(&self.data)
412    }
413}
414
415/// Total difficulty for a block
416#[derive(Debug, Clone)]
417pub struct TotalDifficulty {
418    /// The total difficulty as U256
419    pub value: U256,
420}
421
422impl TotalDifficulty {
423    /// Create a new [`TotalDifficulty`] from a U256 value
424    pub const fn new(value: U256) -> Self {
425        Self { value }
426    }
427
428    /// Convert to an [`Entry`]
429    pub fn to_entry(&self) -> Entry {
430        let mut data = [0u8; 32];
431
432        let be_bytes = self.value.to_be_bytes_vec();
433
434        if be_bytes.len() <= 32 {
435            data[32 - be_bytes.len()..].copy_from_slice(&be_bytes);
436        } else {
437            data.copy_from_slice(&be_bytes[be_bytes.len() - 32..]);
438        }
439
440        Entry::new(TOTAL_DIFFICULTY, data.to_vec())
441    }
442
443    /// Create from an [`Entry`]
444    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
445        if entry.entry_type != TOTAL_DIFFICULTY {
446            return Err(E2sError::Ssz(format!(
447                "Invalid entry type for TotalDifficulty: expected {:02x}{:02x}, got {:02x}{:02x}",
448                TOTAL_DIFFICULTY[0], TOTAL_DIFFICULTY[1], entry.entry_type[0], entry.entry_type[1]
449            )));
450        }
451
452        if entry.data.len() != 32 {
453            return Err(E2sError::Ssz(format!(
454                "Invalid data length for TotalDifficulty: expected 32, got {}",
455                entry.data.len()
456            )));
457        }
458
459        // Convert 32-byte array to U256
460        let value = U256::from_be_slice(&entry.data);
461
462        Ok(Self { value })
463    }
464}
465
466/// Accumulator is computed by constructing an SSZ list of header-records
467/// and calculating the `hash_tree_root`
468#[derive(Debug, Clone)]
469pub struct Accumulator {
470    /// The accumulator root hash
471    pub root: B256,
472}
473
474impl Accumulator {
475    /// Create a new [`Accumulator`] from a root hash
476    pub const fn new(root: B256) -> Self {
477        Self { root }
478    }
479
480    /// Convert to an [`Entry`]
481    pub fn to_entry(&self) -> Entry {
482        Entry::new(ACCUMULATOR, self.root.to_vec())
483    }
484
485    /// Create from an [`Entry`]
486    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
487        if entry.entry_type != ACCUMULATOR {
488            return Err(E2sError::Ssz(format!(
489                "Invalid entry type for Accumulator: expected {:02x}{:02x}, got {:02x}{:02x}",
490                ACCUMULATOR[0], ACCUMULATOR[1], entry.entry_type[0], entry.entry_type[1]
491            )));
492        }
493
494        if entry.data.len() != 32 {
495            return Err(E2sError::Ssz(format!(
496                "Invalid data length for Accumulator: expected 32, got {}",
497                entry.data.len()
498            )));
499        }
500
501        let mut root = [0u8; 32];
502        root.copy_from_slice(&entry.data);
503
504        Ok(Self { root: B256::from(root) })
505    }
506}
507
508/// A block tuple in an Era1 file, containing all components for a single block
509#[derive(Debug, Clone)]
510pub struct BlockTuple {
511    /// Compressed block header
512    pub header: CompressedHeader,
513
514    /// Compressed block body
515    pub body: CompressedBody,
516
517    /// Compressed receipts
518    pub receipts: CompressedReceipts,
519
520    /// Total difficulty
521    pub total_difficulty: TotalDifficulty,
522}
523
524impl BlockTuple {
525    /// Create a new [`BlockTuple`]
526    pub const fn new(
527        header: CompressedHeader,
528        body: CompressedBody,
529        receipts: CompressedReceipts,
530        total_difficulty: TotalDifficulty,
531    ) -> Self {
532        Self { header, body, receipts, total_difficulty }
533    }
534
535    /// Convert to an `alloy_consensus::Block`
536    pub fn to_alloy_block<T: Decodable>(&self) -> Result<Block<T>, E2sError> {
537        let header: Header = self.header.decode()?;
538        let body: BlockBody<T> = self.body.decode()?;
539
540        Ok(Block::new(header, body))
541    }
542
543    /// Create from an `alloy_consensus::Block`
544    pub fn from_alloy_block<T: Encodable, R: Encodable>(
545        block: &Block<T>,
546        receipts: &R,
547        total_difficulty: U256,
548    ) -> Result<Self, E2sError> {
549        let header = CompressedHeader::from_header(&block.header)?;
550        let body = CompressedBody::from_body(&block.body)?;
551
552        let compressed_receipts = CompressedReceipts::from_encodable(receipts)?;
553
554        let difficulty = TotalDifficulty::new(total_difficulty);
555
556        Ok(Self::new(header, body, compressed_receipts, difficulty))
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563    use crate::test_utils::{create_header, create_test_receipt, create_test_receipts};
564    use alloy_eips::eip4895::Withdrawals;
565    use alloy_primitives::{Bytes, U256};
566    use reth_ethereum_primitives::{Receipt, TxType};
567
568    #[test]
569    fn test_header_conversion_roundtrip() {
570        let header = create_header();
571
572        let compressed_header = CompressedHeader::from_header(&header).unwrap();
573
574        let decoded_header = compressed_header.decode_header().unwrap();
575
576        assert_eq!(header.number, decoded_header.number);
577        assert_eq!(header.difficulty, decoded_header.difficulty);
578        assert_eq!(header.timestamp, decoded_header.timestamp);
579        assert_eq!(header.gas_used, decoded_header.gas_used);
580        assert_eq!(header.parent_hash, decoded_header.parent_hash);
581        assert_eq!(header.base_fee_per_gas, decoded_header.base_fee_per_gas);
582    }
583
584    #[test]
585    fn test_block_body_conversion() {
586        let block_body: BlockBody<Bytes> =
587            BlockBody { transactions: vec![], ommers: vec![], withdrawals: None };
588
589        let compressed_body = CompressedBody::from_body(&block_body).unwrap();
590
591        let decoded_body: BlockBody<Bytes> = compressed_body.decode_body().unwrap();
592
593        assert_eq!(decoded_body.transactions.len(), 0);
594        assert_eq!(decoded_body.ommers.len(), 0);
595        assert_eq!(decoded_body.withdrawals, None);
596    }
597
598    #[test]
599    fn test_total_difficulty_roundtrip() {
600        let value = U256::from(123456789u64);
601
602        let total_difficulty = TotalDifficulty::new(value);
603
604        let entry = total_difficulty.to_entry();
605
606        assert_eq!(entry.entry_type, TOTAL_DIFFICULTY);
607
608        let recovered = TotalDifficulty::from_entry(&entry).unwrap();
609
610        assert_eq!(recovered.value, value);
611    }
612
613    #[test]
614    fn test_compression_roundtrip() {
615        let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
616
617        // Test header compression/decompression
618        let compressed_header = CompressedHeader::from_rlp(&rlp_data).unwrap();
619        let decompressed = compressed_header.decompress().unwrap();
620        assert_eq!(decompressed, rlp_data);
621
622        // Test body compression/decompression
623        let compressed_body = CompressedBody::from_rlp(&rlp_data).unwrap();
624        let decompressed = compressed_body.decompress().unwrap();
625        assert_eq!(decompressed, rlp_data);
626
627        // Test receipts compression/decompression
628        let compressed_receipts = CompressedReceipts::from_rlp(&rlp_data).unwrap();
629        let decompressed = compressed_receipts.decompress().unwrap();
630        assert_eq!(decompressed, rlp_data);
631    }
632
633    #[test]
634    fn test_block_tuple_with_data() {
635        // Create block with transactions and withdrawals
636        let header = create_header();
637
638        let transactions = vec![Bytes::from(vec![1, 2, 3, 4]), Bytes::from(vec![5, 6, 7, 8])];
639
640        let withdrawals = Some(Withdrawals(vec![]));
641
642        let block_body = BlockBody { transactions, ommers: vec![], withdrawals };
643
644        let block = Block::new(header, block_body);
645
646        let receipts: Vec<u8> = Vec::new();
647
648        let block_tuple =
649            BlockTuple::from_alloy_block(&block, &receipts, U256::from(123456u64)).unwrap();
650
651        // Convert back to Block
652        let decoded_block: Block<Bytes> = block_tuple.to_alloy_block().unwrap();
653
654        // Verify block components
655        assert_eq!(decoded_block.header.number, 100);
656        assert_eq!(decoded_block.body.transactions.len(), 2);
657        assert_eq!(decoded_block.body.transactions[0], Bytes::from(vec![1, 2, 3, 4]));
658        assert_eq!(decoded_block.body.transactions[1], Bytes::from(vec![5, 6, 7, 8]));
659        assert!(decoded_block.body.withdrawals.is_some());
660    }
661
662    #[test]
663    fn test_single_receipt_compression_roundtrip() {
664        let test_receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
665
666        // Compress the receipt
667        let compressed_receipts =
668            CompressedReceipts::from_encodable(&test_receipt).expect("Failed to compress receipt");
669
670        // Verify compression
671        assert!(!compressed_receipts.data.is_empty());
672
673        // Decode the compressed receipt back
674        let decoded_receipt: Receipt =
675            compressed_receipts.decode().expect("Failed to decode compressed receipt");
676
677        // Verify that the decoded receipt matches the original
678        assert_eq!(decoded_receipt.tx_type, test_receipt.tx_type);
679        assert_eq!(decoded_receipt.success, test_receipt.success);
680        assert_eq!(decoded_receipt.cumulative_gas_used, test_receipt.cumulative_gas_used);
681        assert_eq!(decoded_receipt.logs.len(), test_receipt.logs.len());
682
683        // Verify each log
684        for (original_log, decoded_log) in test_receipt.logs.iter().zip(decoded_receipt.logs.iter())
685        {
686            assert_eq!(decoded_log.address, original_log.address);
687            assert_eq!(decoded_log.data.topics(), original_log.data.topics());
688        }
689    }
690
691    #[test]
692    fn test_receipt_list_compression() {
693        let receipts = create_test_receipts();
694
695        // Compress the list of receipts
696        let compressed_receipts = CompressedReceipts::from_encodable_list(&receipts)
697            .expect("Failed to compress receipt list");
698
699        // Decode the compressed receipts back
700        // Note: most likely the decoding for real era files will be done to reach
701        // `Vec<ReceiptWithBloom>``
702        let decoded_receipts: Vec<Receipt> =
703            compressed_receipts.decode().expect("Failed to decode compressed receipt list");
704
705        // Verify that the decoded receipts match the original
706        assert_eq!(decoded_receipts.len(), receipts.len());
707
708        for (original, decoded) in receipts.iter().zip(decoded_receipts.iter()) {
709            assert_eq!(decoded.tx_type, original.tx_type);
710            assert_eq!(decoded.success, original.success);
711            assert_eq!(decoded.cumulative_gas_used, original.cumulative_gas_used);
712            assert_eq!(decoded.logs.len(), original.logs.len());
713
714            for (original_log, decoded_log) in original.logs.iter().zip(decoded.logs.iter()) {
715                assert_eq!(decoded_log.address, original_log.address);
716                assert_eq!(decoded_log.data.topics(), original_log.data.topics());
717            }
718        }
719    }
720}