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