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