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