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