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 header: {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 header: {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        let mut data = [0u8; 32];
429
430        let be_bytes = self.value.to_be_bytes_vec();
431
432        if be_bytes.len() <= 32 {
433            data[32 - be_bytes.len()..].copy_from_slice(&be_bytes);
434        } else {
435            data.copy_from_slice(&be_bytes[be_bytes.len() - 32..]);
436        }
437
438        Entry::new(TOTAL_DIFFICULTY, data.to_vec())
439    }
440
441    /// Create from an [`Entry`]
442    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
443        if entry.entry_type != TOTAL_DIFFICULTY {
444            return Err(E2sError::Ssz(format!(
445                "Invalid entry type for TotalDifficulty: expected {:02x}{:02x}, got {:02x}{:02x}",
446                TOTAL_DIFFICULTY[0], TOTAL_DIFFICULTY[1], entry.entry_type[0], entry.entry_type[1]
447            )));
448        }
449
450        if entry.data.len() != 32 {
451            return Err(E2sError::Ssz(format!(
452                "Invalid data length for TotalDifficulty: expected 32, got {}",
453                entry.data.len()
454            )));
455        }
456
457        // Convert 32-byte array to U256
458        let value = U256::from_be_slice(&entry.data);
459
460        Ok(Self { value })
461    }
462}
463
464/// Accumulator is computed by constructing an SSZ list of header-records
465/// and calculating the `hash_tree_root`
466#[derive(Debug, Clone)]
467pub struct Accumulator {
468    /// The accumulator root hash
469    pub root: B256,
470}
471
472impl Accumulator {
473    /// Create a new [`Accumulator`] from a root hash
474    pub const fn new(root: B256) -> Self {
475        Self { root }
476    }
477
478    /// Convert to an [`Entry`]
479    pub fn to_entry(&self) -> Entry {
480        Entry::new(ACCUMULATOR, self.root.to_vec())
481    }
482
483    /// Create from an [`Entry`]
484    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
485        if entry.entry_type != ACCUMULATOR {
486            return Err(E2sError::Ssz(format!(
487                "Invalid entry type for Accumulator: expected {:02x}{:02x}, got {:02x}{:02x}",
488                ACCUMULATOR[0], ACCUMULATOR[1], entry.entry_type[0], entry.entry_type[1]
489            )));
490        }
491
492        if entry.data.len() != 32 {
493            return Err(E2sError::Ssz(format!(
494                "Invalid data length for Accumulator: expected 32, got {}",
495                entry.data.len()
496            )));
497        }
498
499        let mut root = [0u8; 32];
500        root.copy_from_slice(&entry.data);
501
502        Ok(Self { root: B256::from(root) })
503    }
504}
505
506/// A block tuple in an Era1 file, containing all components for a single block
507#[derive(Debug, Clone)]
508pub struct BlockTuple {
509    /// Compressed block header
510    pub header: CompressedHeader,
511
512    /// Compressed block body
513    pub body: CompressedBody,
514
515    /// Compressed receipts
516    pub receipts: CompressedReceipts,
517
518    /// Total difficulty
519    pub total_difficulty: TotalDifficulty,
520}
521
522impl BlockTuple {
523    /// Create a new [`BlockTuple`]
524    pub const fn new(
525        header: CompressedHeader,
526        body: CompressedBody,
527        receipts: CompressedReceipts,
528        total_difficulty: TotalDifficulty,
529    ) -> Self {
530        Self { header, body, receipts, total_difficulty }
531    }
532
533    /// Convert to an `alloy_consensus::Block`
534    pub fn to_alloy_block<T: Decodable>(&self) -> Result<Block<T>, E2sError> {
535        let header: Header = self.header.decode()?;
536        let body: BlockBody<T> = self.body.decode()?;
537
538        Ok(Block::new(header, body))
539    }
540
541    /// Create from an `alloy_consensus::Block`
542    pub fn from_alloy_block<T: Encodable, R: Encodable>(
543        block: &Block<T>,
544        receipts: &R,
545        total_difficulty: U256,
546    ) -> Result<Self, E2sError> {
547        let header = CompressedHeader::from_header(&block.header)?;
548        let body = CompressedBody::from_body(&block.body)?;
549
550        let compressed_receipts = CompressedReceipts::from_encodable(receipts)?;
551
552        let difficulty = TotalDifficulty::new(total_difficulty);
553
554        Ok(Self::new(header, body, compressed_receipts, difficulty))
555    }
556}
557
558#[cfg(test)]
559mod tests {
560    use super::*;
561    use crate::test_utils::{create_header, create_test_receipt, create_test_receipts};
562    use alloy_eips::eip4895::Withdrawals;
563    use alloy_primitives::{Bytes, U256};
564    use reth_ethereum_primitives::{Receipt, TxType};
565
566    #[test]
567    fn test_header_conversion_roundtrip() {
568        let header = create_header();
569
570        let compressed_header = CompressedHeader::from_header(&header).unwrap();
571
572        let decoded_header = compressed_header.decode_header().unwrap();
573
574        assert_eq!(header.number, decoded_header.number);
575        assert_eq!(header.difficulty, decoded_header.difficulty);
576        assert_eq!(header.timestamp, decoded_header.timestamp);
577        assert_eq!(header.gas_used, decoded_header.gas_used);
578        assert_eq!(header.parent_hash, decoded_header.parent_hash);
579        assert_eq!(header.base_fee_per_gas, decoded_header.base_fee_per_gas);
580    }
581
582    #[test]
583    fn test_block_body_conversion() {
584        let block_body: BlockBody<Bytes> =
585            BlockBody { transactions: vec![], ommers: vec![], withdrawals: None };
586
587        let compressed_body = CompressedBody::from_body(&block_body).unwrap();
588
589        let decoded_body: BlockBody<Bytes> = compressed_body.decode_body().unwrap();
590
591        assert_eq!(decoded_body.transactions.len(), 0);
592        assert_eq!(decoded_body.ommers.len(), 0);
593        assert_eq!(decoded_body.withdrawals, None);
594    }
595
596    #[test]
597    fn test_total_difficulty_roundtrip() {
598        let value = U256::from(123456789u64);
599
600        let total_difficulty = TotalDifficulty::new(value);
601
602        let entry = total_difficulty.to_entry();
603
604        assert_eq!(entry.entry_type, TOTAL_DIFFICULTY);
605
606        let recovered = TotalDifficulty::from_entry(&entry).unwrap();
607
608        assert_eq!(recovered.value, value);
609    }
610
611    #[test]
612    fn test_compression_roundtrip() {
613        let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
614
615        // Test header compression/decompression
616        let compressed_header = CompressedHeader::from_rlp(&rlp_data).unwrap();
617        let decompressed = compressed_header.decompress().unwrap();
618        assert_eq!(decompressed, rlp_data);
619
620        // Test body compression/decompression
621        let compressed_body = CompressedBody::from_rlp(&rlp_data).unwrap();
622        let decompressed = compressed_body.decompress().unwrap();
623        assert_eq!(decompressed, rlp_data);
624
625        // Test receipts compression/decompression
626        let compressed_receipts = CompressedReceipts::from_rlp(&rlp_data).unwrap();
627        let decompressed = compressed_receipts.decompress().unwrap();
628        assert_eq!(decompressed, rlp_data);
629    }
630
631    #[test]
632    fn test_block_tuple_with_data() {
633        // Create block with transactions and withdrawals
634        let header = create_header();
635
636        let transactions = vec![Bytes::from(vec![1, 2, 3, 4]), Bytes::from(vec![5, 6, 7, 8])];
637
638        let withdrawals = Some(Withdrawals(vec![]));
639
640        let block_body = BlockBody { transactions, ommers: vec![], withdrawals };
641
642        let block = Block::new(header, block_body);
643
644        let receipts: Vec<u8> = Vec::new();
645
646        let block_tuple =
647            BlockTuple::from_alloy_block(&block, &receipts, U256::from(123456u64)).unwrap();
648
649        // Convert back to Block
650        let decoded_block: Block<Bytes> = block_tuple.to_alloy_block().unwrap();
651
652        // Verify block components
653        assert_eq!(decoded_block.header.number, 100);
654        assert_eq!(decoded_block.body.transactions.len(), 2);
655        assert_eq!(decoded_block.body.transactions[0], Bytes::from(vec![1, 2, 3, 4]));
656        assert_eq!(decoded_block.body.transactions[1], Bytes::from(vec![5, 6, 7, 8]));
657        assert!(decoded_block.body.withdrawals.is_some());
658    }
659
660    #[test]
661    fn test_single_receipt_compression_roundtrip() {
662        let test_receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
663
664        // Compress the receipt
665        let compressed_receipts =
666            CompressedReceipts::from_encodable(&test_receipt).expect("Failed to compress receipt");
667
668        // Verify compression
669        assert!(!compressed_receipts.data.is_empty());
670
671        // Decode the compressed receipt back
672        let decoded_receipt: Receipt =
673            compressed_receipts.decode().expect("Failed to decode compressed receipt");
674
675        // Verify that the decoded receipt matches the original
676        assert_eq!(decoded_receipt.tx_type, test_receipt.tx_type);
677        assert_eq!(decoded_receipt.success, test_receipt.success);
678        assert_eq!(decoded_receipt.cumulative_gas_used, test_receipt.cumulative_gas_used);
679        assert_eq!(decoded_receipt.logs.len(), test_receipt.logs.len());
680
681        // Verify each log
682        for (original_log, decoded_log) in test_receipt.logs.iter().zip(decoded_receipt.logs.iter())
683        {
684            assert_eq!(decoded_log.address, original_log.address);
685            assert_eq!(decoded_log.data.topics(), original_log.data.topics());
686        }
687    }
688
689    #[test]
690    fn test_receipt_list_compression() {
691        let receipts = create_test_receipts();
692
693        // Compress the list of receipts
694        let compressed_receipts = CompressedReceipts::from_encodable_list(&receipts)
695            .expect("Failed to compress receipt list");
696
697        // Decode the compressed receipts back
698        // Note: For real ERA1 files, use `Vec<ReceiptWithBloom>` before Era ~1520 or use
699        // `Vec<ReceiptEnvelope>` after this era
700        let decoded_receipts: Vec<Receipt> =
701            compressed_receipts.decode().expect("Failed to decode compressed receipt list");
702
703        // Verify that the decoded receipts match the original
704        assert_eq!(decoded_receipts.len(), receipts.len());
705
706        for (original, decoded) in receipts.iter().zip(decoded_receipts.iter()) {
707            assert_eq!(decoded.tx_type, original.tx_type);
708            assert_eq!(decoded.success, original.success);
709            assert_eq!(decoded.cumulative_gas_used, original.cumulative_gas_used);
710            assert_eq!(decoded.logs.len(), original.logs.len());
711
712            for (original_log, decoded_log) in original.logs.iter().zip(decoded.logs.iter()) {
713                assert_eq!(decoded_log.address, original_log.address);
714                assert_eq!(decoded_log.data.topics(), original_log.data.topics());
715            }
716        }
717    }
718}