1use crate::{
14 common::{
15 compression::{snappy_compress, snappy_decompress, SnappyRlpCodec},
16 decode::DecodeCompressedRlp,
17 },
18 e2s::{error::E2sError, types::Entry},
19};
20use alloy_consensus::{Block, BlockBody, Eip658Value, Header, Receipt, ReceiptEnvelope, TxType};
21use alloy_primitives::{Log, B256, U256};
22use alloy_rlp::{Decodable, Encodable, RlpDecodable, RlpEncodable};
23use sha2::{Digest, Sha256};
24
25pub const COMPRESSED_HEADER: [u8; 2] = [0x03, 0x00];
28
29pub const COMPRESSED_BODY: [u8; 2] = [0x04, 0x00];
31
32pub const COMPRESSED_SLIM_RECEIPTS: [u8; 2] = [0x0a, 0x00];
35
36pub const PROOF: [u8; 2] = [0x0b, 0x00];
39
40pub const TOTAL_DIFFICULTY: [u8; 2] = [0x06, 0x00];
42
43pub const ACCUMULATOR: [u8; 2] = [0x07, 0x00];
45
46pub const MAX_BLOCKS_PER_ERE: usize = crate::common::MAX_ENTRIES_PER_ERA as usize;
48
49#[derive(Debug, Clone)]
51pub struct CompressedHeader {
52 pub data: Vec<u8>,
54}
55
56impl CompressedHeader {
57 pub const fn new(data: Vec<u8>) -> Self {
59 Self { data }
60 }
61
62 pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
64 Ok(Self { data: snappy_compress(rlp_data)? })
65 }
66
67 pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
69 snappy_decompress(&self.data)
70 }
71
72 pub fn to_entry(&self) -> Entry {
74 Entry::new(COMPRESSED_HEADER, self.data.clone())
75 }
76
77 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
79 entry.ensure_type(COMPRESSED_HEADER, "CompressedHeader")?;
80 Ok(Self { data: entry.data.clone() })
81 }
82
83 pub fn decode_header(&self) -> Result<Header, E2sError> {
85 self.decode()
86 }
87
88 pub fn from_header<H: Encodable>(header: &H) -> Result<Self, E2sError> {
90 let encoder = SnappyRlpCodec::new();
91 let compressed = encoder.encode(header)?;
92 Ok(Self::new(compressed))
93 }
94}
95
96impl DecodeCompressedRlp for CompressedHeader {
97 fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
98 let decoder = SnappyRlpCodec::<T>::new();
99 decoder.decode(&self.data)
100 }
101}
102
103#[derive(Debug, Clone)]
105pub struct CompressedBody {
106 pub data: Vec<u8>,
108}
109
110impl CompressedBody {
111 pub const fn new(data: Vec<u8>) -> Self {
113 Self { data }
114 }
115
116 pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
118 Ok(Self { data: snappy_compress(rlp_data)? })
119 }
120
121 pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
123 snappy_decompress(&self.data)
124 }
125
126 pub fn to_entry(&self) -> Entry {
128 Entry::new(COMPRESSED_BODY, self.data.clone())
129 }
130
131 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
133 entry.ensure_type(COMPRESSED_BODY, "CompressedBody")?;
134 Ok(Self { data: entry.data.clone() })
135 }
136
137 pub fn decode_body<T: Decodable, H: Decodable>(&self) -> Result<BlockBody<T, H>, E2sError> {
139 let decompressed = self.decompress()?;
140 Self::decode_body_from_decompressed(&decompressed)
141 }
142
143 pub fn decode_body_from_decompressed<T: Decodable, H: Decodable>(
145 data: &[u8],
146 ) -> Result<BlockBody<T, H>, E2sError> {
147 alloy_rlp::decode_exact::<BlockBody<T, H>>(data)
148 .map_err(|e| E2sError::Rlp(format!("Failed to decode RLP data: {e}")))
149 }
150
151 pub fn from_body<B: Encodable>(body: &B) -> Result<Self, E2sError> {
153 let encoder = SnappyRlpCodec::new();
154 let compressed = encoder.encode(body)?;
155 Ok(Self::new(compressed))
156 }
157}
158
159impl DecodeCompressedRlp for CompressedBody {
160 fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
161 let decoder = SnappyRlpCodec::<T>::new();
162 decoder.decode(&self.data)
163 }
164}
165
166#[derive(Debug, Clone)]
171pub struct CompressedSlimReceipts {
172 pub data: Vec<u8>,
174}
175
176impl CompressedSlimReceipts {
177 pub const fn new(data: Vec<u8>) -> Self {
179 Self { data }
180 }
181
182 pub fn from_rlp(rlp_data: &[u8]) -> Result<Self, E2sError> {
184 Ok(Self { data: snappy_compress(rlp_data)? })
185 }
186
187 pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
189 snappy_decompress(&self.data)
190 }
191
192 pub fn to_entry(&self) -> Entry {
194 Entry::new(COMPRESSED_SLIM_RECEIPTS, self.data.clone())
195 }
196
197 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
199 entry.ensure_type(COMPRESSED_SLIM_RECEIPTS, "CompressedSlimReceipts")?;
200 Ok(Self { data: entry.data.clone() })
201 }
202
203 pub fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
205 let decoder = SnappyRlpCodec::<T>::new();
206 decoder.decode(&self.data)
207 }
208
209 pub fn from_encodable<T: Encodable>(data: &T) -> Result<Self, E2sError> {
211 let encoder = SnappyRlpCodec::<T>::new();
212 let compressed = encoder.encode(data)?;
213 Ok(Self::new(compressed))
214 }
215
216 pub fn from_encodable_list<T: Encodable>(receipts: &[T]) -> Result<Self, E2sError> {
218 let mut rlp_data = Vec::new();
219 alloy_rlp::encode_list(receipts, &mut rlp_data);
220 Self::from_rlp(&rlp_data)
221 }
222
223 pub fn from_receipts(receipts: &[SlimReceipt]) -> Result<Self, E2sError> {
228 Self::from_encodable_list(receipts)
229 }
230
231 pub fn decode_receipts(&self) -> Result<Vec<SlimReceipt>, E2sError> {
233 self.decode()
234 }
235}
236
237impl DecodeCompressedRlp for CompressedSlimReceipts {
238 fn decode<T: Decodable>(&self) -> Result<T, E2sError> {
239 let decoder = SnappyRlpCodec::<T>::new();
240 decoder.decode(&self.data)
241 }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, RlpEncodable, RlpDecodable)]
252pub struct SlimReceipt {
253 pub tx_type: TxType,
255 pub status: Eip658Value,
257 pub cumulative_gas_used: u64,
259 pub logs: Vec<Log>,
261}
262
263impl From<SlimReceipt> for ReceiptEnvelope {
264 fn from(receipt: SlimReceipt) -> Self {
266 let SlimReceipt { tx_type, status, cumulative_gas_used, logs } = receipt;
267 let receipt = Receipt { status, cumulative_gas_used, logs };
268
269 Self::from_typed(tx_type, receipt.with_bloom())
270 }
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
277#[repr(u8)]
278pub enum ProofType {
279 BlockProofHistoricalHashesAccumulator = 0,
281 BlockProofHistoricalRoots = 1,
283 BlockProofHistoricalSummariesCapella = 2,
285 BlockProofHistoricalSummariesDeneb = 3,
287}
288
289impl ProofType {
290 pub const fn from_byte(value: u8) -> Option<Self> {
292 match value {
293 0 => Some(Self::BlockProofHistoricalHashesAccumulator),
294 1 => Some(Self::BlockProofHistoricalRoots),
295 2 => Some(Self::BlockProofHistoricalSummariesCapella),
296 3 => Some(Self::BlockProofHistoricalSummariesDeneb),
297 _ => None,
298 }
299 }
300
301 pub const fn as_byte(self) -> u8 {
303 self as u8
304 }
305}
306
307#[derive(Debug, Clone)]
313pub struct Proof {
314 pub data: Vec<u8>,
316}
317
318impl Proof {
319 pub const fn new(data: Vec<u8>) -> Self {
321 Self { data }
322 }
323
324 pub fn encode(proof_type: ProofType, ssz_proof: &[u8]) -> Result<Self, E2sError> {
326 let mut payload = Vec::new();
329 proof_type.as_byte().encode(&mut payload);
330 ssz_proof.encode(&mut payload);
331
332 let mut rlp_data = Vec::new();
333 alloy_rlp::Header { list: true, payload_length: payload.len() }.encode(&mut rlp_data);
334 rlp_data.extend_from_slice(&payload);
335
336 Ok(Self { data: snappy_compress(&rlp_data)? })
337 }
338
339 pub fn decode(&self) -> Result<(ProofType, Vec<u8>), E2sError> {
341 let decompressed = snappy_decompress(&self.data)?;
342
343 let mut buf = decompressed.as_slice();
344 let header = alloy_rlp::Header::decode(&mut buf)
345 .map_err(|e| E2sError::Rlp(format!("Failed to decode proof RLP header: {e}")))?;
346 if !header.list {
347 return Err(E2sError::Rlp("Expected RLP list for Proof entry".to_string()));
348 }
349
350 if buf.len() != header.payload_length {
354 return Err(E2sError::Rlp(format!(
355 "Trailing bytes after Proof list: {} byte(s) beyond the list payload",
356 buf.len().saturating_sub(header.payload_length)
357 )));
358 }
359 let mut payload = &buf[..header.payload_length];
360
361 let proof_type_byte = u8::decode(&mut payload)
362 .map_err(|e| E2sError::Rlp(format!("Failed to decode proof type: {e}")))?;
363 let proof_type = ProofType::from_byte(proof_type_byte)
364 .ok_or_else(|| E2sError::Rlp(format!("Unknown proof type: {proof_type_byte}")))?;
365
366 let ssz_bytes = alloy_rlp::Header::decode_bytes(&mut payload, false)
367 .map_err(|e| E2sError::Rlp(format!("Failed to decode proof SSZ bytes: {e}")))?
368 .to_vec();
369
370 if !payload.is_empty() {
371 return Err(E2sError::Rlp("Unexpected extra items in Proof list".to_string()));
372 }
373
374 Ok((proof_type, ssz_bytes))
375 }
376
377 pub fn to_entry(&self) -> Entry {
379 Entry::new(PROOF, self.data.clone())
380 }
381
382 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
384 entry.ensure_type(PROOF, "Proof")?;
385 Ok(Self { data: entry.data.clone() })
386 }
387}
388
389#[derive(Debug, Clone)]
391pub struct TotalDifficulty {
392 pub value: U256,
394}
395
396impl TotalDifficulty {
397 pub const fn new(value: U256) -> Self {
399 Self { value }
400 }
401
402 pub fn to_entry(&self) -> Entry {
404 let data = self.value.to_le_bytes::<32>().to_vec();
406 Entry::new(TOTAL_DIFFICULTY, data)
407 }
408
409 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
411 entry.ensure_type(TOTAL_DIFFICULTY, "TotalDifficulty")?;
412
413 if entry.data.len() != 32 {
414 return Err(E2sError::Ssz(format!(
415 "Invalid data length for TotalDifficulty: expected 32, got {}",
416 entry.data.len()
417 )));
418 }
419
420 let value = U256::from_le_slice(&entry.data);
422
423 Ok(Self { value })
424 }
425}
426
427#[derive(Debug, Clone, PartialEq, Eq)]
430pub struct Accumulator {
431 pub root: B256,
433}
434
435impl Accumulator {
436 pub const fn new(root: B256) -> Self {
438 Self { root }
439 }
440
441 pub fn to_entry(&self) -> Entry {
443 Entry::new(ACCUMULATOR, self.root.to_vec())
444 }
445
446 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
448 entry.ensure_type(ACCUMULATOR, "Accumulator")?;
449
450 if entry.data.len() != 32 {
451 return Err(E2sError::Ssz(format!(
452 "Invalid data length for Accumulator: expected 32, got {}",
453 entry.data.len()
454 )));
455 }
456
457 let mut root = [0u8; 32];
458 root.copy_from_slice(&entry.data);
459
460 Ok(Self { root: B256::from(root) })
461 }
462
463 pub fn from_header_records(records: &[HeaderRecord]) -> Result<Self, E2sError> {
473 let capacity = MAX_BLOCKS_PER_ERE;
474
475 if records.len() > capacity {
476 return Err(E2sError::Ssz(format!(
477 "Too many header records: got {}, max {}",
478 records.len(),
479 capacity
480 )));
481 }
482
483 let mut leaves = Vec::with_capacity(capacity);
485 for record in records {
486 let mut data = [0u8; 64];
487 data[..32].copy_from_slice(record.block_hash.as_slice());
488 data[32..].copy_from_slice(&record.total_difficulty.to_le_bytes::<32>());
489 leaves.push(<[u8; 32]>::from(Sha256::digest(data)));
490 }
491
492 leaves.resize(capacity, [0u8; 32]);
494
495 while leaves.len() > 1 {
497 let mut next_level = Vec::with_capacity(leaves.len() / 2);
498 for pair in leaves.as_chunks::<2>().0 {
499 let mut data = [0u8; 64];
500 data[..32].copy_from_slice(&pair[0]);
501 data[32..].copy_from_slice(&pair[1]);
502 next_level.push(<[u8; 32]>::from(Sha256::digest(data)));
503 }
504 leaves = next_level;
505 }
506
507 let merkle_root = leaves[0];
508
509 let mut mix = [0u8; 64];
511 mix[..32].copy_from_slice(&merkle_root);
512 let length = records.len() as u64;
513 mix[32..40].copy_from_slice(&length.to_le_bytes());
514 Ok(Self { root: B256::from(<[u8; 32]>::from(Sha256::digest(mix))) })
517 }
518}
519
520#[derive(Debug, Clone)]
529pub struct HeaderRecord {
530 pub block_hash: B256,
533 pub total_difficulty: U256,
536}
537
538#[derive(Debug, Clone)]
547pub struct BlockTuple {
548 pub header: CompressedHeader,
550
551 pub body: CompressedBody,
553
554 pub receipts: Option<CompressedSlimReceipts>,
556
557 pub total_difficulty: Option<TotalDifficulty>,
559
560 pub proof: Option<Proof>,
562}
563
564impl BlockTuple {
565 pub const fn new(header: CompressedHeader, body: CompressedBody) -> Self {
571 Self { header, body, receipts: None, total_difficulty: None, proof: None }
572 }
573
574 pub fn with_receipts(mut self, receipts: CompressedSlimReceipts) -> Self {
576 self.receipts = Some(receipts);
577 self
578 }
579
580 pub const fn with_total_difficulty(mut self, total_difficulty: TotalDifficulty) -> Self {
582 self.total_difficulty = Some(total_difficulty);
583 self
584 }
585
586 pub fn with_proof(mut self, proof: Proof) -> Self {
588 self.proof = Some(proof);
589 self
590 }
591
592 pub const fn component_count(&self) -> u64 {
595 2 + self.receipts.is_some() as u64 +
596 self.total_difficulty.is_some() as u64 +
597 self.proof.is_some() as u64
598 }
599
600 pub fn to_alloy_block<T: Decodable>(&self) -> Result<Block<T>, E2sError> {
602 let header: Header = self.header.decode()?;
603 let body: BlockBody<T> = self.body.decode()?;
604
605 Ok(Block::new(header, body))
606 }
607
608 pub fn from_alloy_block<T: Encodable, R: Encodable>(
610 block: &Block<T>,
611 receipts: &R,
612 total_difficulty: U256,
613 ) -> Result<Self, E2sError> {
614 let header = CompressedHeader::from_header(&block.header)?;
615 let body = CompressedBody::from_body(&block.body)?;
616
617 let compressed_receipts = CompressedSlimReceipts::from_encodable(receipts)?;
618
619 let difficulty = TotalDifficulty::new(total_difficulty);
620
621 Ok(Self::new(header, body)
622 .with_receipts(compressed_receipts)
623 .with_total_difficulty(difficulty))
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use crate::test_utils::{create_header, create_test_receipt, create_test_receipts};
631 use alloy_eips::eip4895::Withdrawals;
632 use alloy_primitives::{Bytes, U256};
633 use reth_ethereum_primitives::{Receipt, TxType};
634
635 #[test]
636 fn test_header_conversion_roundtrip() {
637 let header = create_header();
638
639 let compressed_header = CompressedHeader::from_header(&header).unwrap();
640
641 let decoded_header = compressed_header.decode_header().unwrap();
642
643 assert_eq!(header.number, decoded_header.number);
644 assert_eq!(header.difficulty, decoded_header.difficulty);
645 assert_eq!(header.timestamp, decoded_header.timestamp);
646 assert_eq!(header.gas_used, decoded_header.gas_used);
647 assert_eq!(header.parent_hash, decoded_header.parent_hash);
648 assert_eq!(header.base_fee_per_gas, decoded_header.base_fee_per_gas);
649 }
650
651 #[test]
652 fn test_block_body_conversion() {
653 let block_body: BlockBody<Bytes> =
654 BlockBody { transactions: vec![], ommers: vec![], withdrawals: None };
655
656 let compressed_body = CompressedBody::from_body(&block_body).unwrap();
657
658 let decoded_body: BlockBody<Bytes> = compressed_body.decode_body().unwrap();
659
660 assert_eq!(decoded_body.transactions.len(), 0);
661 assert_eq!(decoded_body.ommers.len(), 0);
662 assert_eq!(decoded_body.withdrawals, None);
663 }
664
665 #[test]
666 fn test_total_difficulty_roundtrip() {
667 let value = U256::from(123456789u64);
668
669 let total_difficulty = TotalDifficulty::new(value);
670
671 let entry = total_difficulty.to_entry();
672
673 assert_eq!(entry.entry_type, TOTAL_DIFFICULTY);
674
675 let recovered = TotalDifficulty::from_entry(&entry).unwrap();
676
677 assert_eq!(recovered.value, value);
678 }
679
680 #[test]
681 fn test_total_difficulty_ssz_le_encoding() {
682 let value = U256::from(1u64);
685 let td = TotalDifficulty::new(value);
686 let entry = td.to_entry();
687
688 assert_eq!(entry.data[0], 1, "First byte must be 1 (little-endian)");
690 assert_eq!(entry.data[31], 0, "Last byte must be 0 (little-endian)");
691 }
692
693 #[test]
694 fn test_compression_roundtrip() {
695 let rlp_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
696
697 let compressed_header = CompressedHeader::from_rlp(&rlp_data).unwrap();
699 let decompressed = compressed_header.decompress().unwrap();
700 assert_eq!(decompressed, rlp_data);
701
702 let compressed_body = CompressedBody::from_rlp(&rlp_data).unwrap();
704 let decompressed = compressed_body.decompress().unwrap();
705 assert_eq!(decompressed, rlp_data);
706
707 let compressed_receipts = CompressedSlimReceipts::from_rlp(&rlp_data).unwrap();
709 let decompressed = compressed_receipts.decompress().unwrap();
710 assert_eq!(decompressed, rlp_data);
711 }
712
713 #[test]
714 fn test_block_tuple_with_data() {
715 let header = create_header();
717
718 let transactions = vec![Bytes::from(vec![1, 2, 3, 4]), Bytes::from(vec![5, 6, 7, 8])];
719
720 let withdrawals = Some(Withdrawals(vec![]));
721
722 let block_body = BlockBody { transactions, ommers: vec![], withdrawals };
723
724 let block = Block::new(header, block_body);
725
726 let receipts: Vec<u8> = Vec::new();
727
728 let block_tuple =
729 BlockTuple::from_alloy_block(&block, &receipts, U256::from(123456u64)).unwrap();
730
731 let decoded_block: Block<Bytes> = block_tuple.to_alloy_block().unwrap();
733
734 assert_eq!(decoded_block.header.number, 100);
736 assert_eq!(decoded_block.body.transactions.len(), 2);
737 assert_eq!(decoded_block.body.transactions[0], Bytes::from(vec![1, 2, 3, 4]));
738 assert_eq!(decoded_block.body.transactions[1], Bytes::from(vec![5, 6, 7, 8]));
739 assert!(decoded_block.body.withdrawals.is_some());
740 }
741
742 #[test]
743 fn test_block_tuple_component_count() {
744 let base = BlockTuple::new(CompressedHeader::new(vec![1]), CompressedBody::new(vec![2]));
745 assert_eq!(base.component_count(), 2);
747
748 assert_eq!(
750 base.clone().with_receipts(CompressedSlimReceipts::new(vec![3])).component_count(),
751 3
752 );
753 let full = base
754 .with_receipts(CompressedSlimReceipts::new(vec![3]))
755 .with_total_difficulty(TotalDifficulty::new(U256::from(1u64)))
756 .with_proof(Proof::new(vec![4]));
757 assert_eq!(full.component_count(), 5);
758 assert!(full.receipts.is_some() && full.total_difficulty.is_some() && full.proof.is_some());
759 }
760
761 #[test]
762 fn test_single_receipt_compression_roundtrip() {
763 let test_receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
764
765 let compressed_receipts = CompressedSlimReceipts::from_encodable(&test_receipt)
767 .expect("Failed to compress receipt");
768
769 assert!(!compressed_receipts.data.is_empty());
771
772 let decoded_receipt: Receipt =
774 compressed_receipts.decode().expect("Failed to decode compressed receipt");
775
776 assert_eq!(decoded_receipt.tx_type, test_receipt.tx_type);
778 assert_eq!(decoded_receipt.success, test_receipt.success);
779 assert_eq!(decoded_receipt.cumulative_gas_used, test_receipt.cumulative_gas_used);
780 assert_eq!(decoded_receipt.logs.len(), test_receipt.logs.len());
781
782 for (original_log, decoded_log) in test_receipt.logs.iter().zip(decoded_receipt.logs.iter())
784 {
785 assert_eq!(decoded_log.address, original_log.address);
786 assert_eq!(decoded_log.data.topics(), original_log.data.topics());
787 }
788 }
789
790 #[test]
791 fn test_slim_receipt_matches_spec_rlp() {
792 let receipt = create_test_receipt(TxType::Eip1559, true, 21000, 2);
796
797 let compressed = CompressedSlimReceipts::from_encodable(&receipt).unwrap();
798 let actual_rlp = compressed.decompress().unwrap();
799
800 let mut fields = Vec::new();
802 (receipt.tx_type as u8).encode(&mut fields);
803 receipt.success.encode(&mut fields);
804 receipt.cumulative_gas_used.encode(&mut fields);
805 receipt.logs.encode(&mut fields);
806 let mut expected = Vec::new();
807 alloy_rlp::Header { list: true, payload_length: fields.len() }.encode(&mut expected);
808 expected.extend_from_slice(&fields);
809
810 assert_eq!(
811 actual_rlp, expected,
812 "slim receipt RLP must be the 4-element list [tx-type, status, cumulative-gas, logs]"
813 );
814 }
815
816 #[test]
817 fn test_slim_receipts_typed_helpers() {
818 let receipts = vec![
821 SlimReceipt {
822 tx_type: TxType::Eip1559,
823 status: Eip658Value::Eip658(true),
824 cumulative_gas_used: 21000,
825 logs: vec![],
826 },
827 SlimReceipt {
828 tx_type: TxType::Legacy,
829 status: Eip658Value::PostState(B256::repeat_byte(0xab)),
830 cumulative_gas_used: 42000,
831 logs: vec![],
832 },
833 ];
834
835 let compressed = CompressedSlimReceipts::from_receipts(&receipts).unwrap();
836 let decoded = compressed.decode_receipts().unwrap();
837
838 assert_eq!(decoded, receipts);
839 }
840
841 #[test]
842 fn test_accumulator_from_header_records_known_vectors() {
843 let expected_empty: B256 =
846 "4a8c3a07c8d23adc5bac61157555c3c784d53d9bc110c1370809bd23cd93777d".parse().unwrap();
847 let expected_single_zero: B256 =
848 "81fd641249670887a731386e756a7a1538dc781b1b0bf016889045d350812817".parse().unwrap();
849 let expected_single_nonzero: B256 =
850 "ada35c48d81117f4fd588554cd4c4752356336e84cb41106dea1ceb4cfac8799".parse().unwrap();
851
852 let acc_empty = Accumulator::from_header_records(&[]).unwrap();
854 assert_eq!(acc_empty.root, expected_empty);
855
856 let records = vec![HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO }];
858 let acc = Accumulator::from_header_records(&records).unwrap();
859 assert_eq!(acc.root, expected_single_zero);
860
861 let records2 = vec![HeaderRecord {
863 block_hash: B256::from([1u8; 32]),
864 total_difficulty: U256::from(100u64),
865 }];
866 let acc2 = Accumulator::from_header_records(&records2).unwrap();
867 assert_eq!(acc2.root, expected_single_nonzero);
868 }
869
870 #[test]
871 fn test_accumulator_rejects_oversized_input() {
872 let records = vec![
873 HeaderRecord { block_hash: B256::ZERO, total_difficulty: U256::ZERO };
874 MAX_BLOCKS_PER_ERE + 1
875 ];
876 assert!(Accumulator::from_header_records(&records).is_err());
877 }
878
879 #[test]
880 fn test_proof_type_byte_roundtrip() {
881 for ty in [
882 ProofType::BlockProofHistoricalHashesAccumulator,
883 ProofType::BlockProofHistoricalRoots,
884 ProofType::BlockProofHistoricalSummariesCapella,
885 ProofType::BlockProofHistoricalSummariesDeneb,
886 ] {
887 assert_eq!(ProofType::from_byte(ty.as_byte()), Some(ty));
888 }
889 assert_eq!(ProofType::from_byte(4), None);
890 }
891
892 #[test]
893 fn test_proof_roundtrip() {
894 let ssz_proof = vec![0xab; 64];
895 let proof = Proof::encode(ProofType::BlockProofHistoricalRoots, &ssz_proof).unwrap();
896
897 let entry = proof.to_entry();
899 assert_eq!(entry.entry_type, PROOF);
900 let recovered = Proof::from_entry(&entry).unwrap();
901
902 let (proof_type, ssz_bytes) = recovered.decode().unwrap();
903 assert_eq!(proof_type, ProofType::BlockProofHistoricalRoots);
904 assert_eq!(ssz_bytes, ssz_proof);
905 }
906
907 #[test]
908 fn test_from_entry_rejects_wrong_type() {
909 let entry = Entry::new(COMPRESSED_BODY, vec![1, 2, 3]);
910 assert!(CompressedHeader::from_entry(&entry).is_err());
911 }
912
913 #[test]
914 fn test_decode_rejects_trailing_bytes() {
915 let mut rlp = Vec::new();
918 7u64.encode(&mut rlp);
919 rlp.push(0xff);
920 let compressed = CompressedHeader::from_rlp(&rlp).unwrap();
921 assert!(compressed.decode::<u64>().is_err());
922 }
923
924 #[test]
925 fn test_proof_decode_rejects_trailing_bytes() {
926 let valid = Proof::encode(ProofType::BlockProofHistoricalRoots, &[1, 2, 3]).unwrap();
927 let mut raw = snappy_decompress(&valid.data).unwrap();
928 raw.push(0xff); let tampered = Proof::new(snappy_compress(&raw).unwrap());
930 assert!(tampered.decode().is_err());
931 }
932
933 #[test]
934 fn test_proof_decode_rejects_extra_list_item() {
935 let mut payload = Vec::new();
937 0u8.encode(&mut payload);
938 alloy_primitives::Bytes::from(vec![1, 2, 3]).encode(&mut payload);
939 99u8.encode(&mut payload);
940 let mut rlp = Vec::new();
941 alloy_rlp::Header { list: true, payload_length: payload.len() }.encode(&mut rlp);
942 rlp.extend_from_slice(&payload);
943 let proof = Proof::new(snappy_compress(&rlp).unwrap());
944 assert!(proof.decode().is_err());
945 }
946
947 #[test]
948 fn test_receipt_list_compression() {
949 let receipts = create_test_receipts();
950
951 let compressed_receipts = CompressedSlimReceipts::from_encodable_list(&receipts)
953 .expect("Failed to compress receipt list");
954
955 let decoded_receipts: Vec<Receipt> =
958 compressed_receipts.decode().expect("Failed to decode compressed receipt list");
959
960 assert_eq!(decoded_receipts.len(), receipts.len());
962
963 for (original, decoded) in receipts.iter().zip(decoded_receipts.iter()) {
964 assert_eq!(decoded.tx_type, original.tx_type);
965 assert_eq!(decoded.success, original.success);
966 assert_eq!(decoded.cumulative_gas_used, original.cumulative_gas_used);
967 assert_eq!(decoded.logs.len(), original.logs.len());
968
969 for (original_log, decoded_log) in original.logs.iter().zip(decoded.logs.iter()) {
970 assert_eq!(decoded_log.address, original_log.address);
971 assert_eq!(decoded_log.data.topics(), original_log.data.topics());
972 }
973 }
974 }
975}