1use crate::e2s::{error::E2sError, types::Entry};
31use alloy_consensus::Block;
32use alloy_eips::eip2718::Decodable2718;
33use alloy_rpc_types_beacon::block::{
34 SignedBeaconBlockAltair, SignedBeaconBlockBellatrix, SignedBeaconBlockCapella,
35 SignedBeaconBlockDeneb, SignedBeaconBlockElectra, SignedBeaconBlockPhase0,
36};
37use alloy_rpc_types_engine::{
38 CancunPayloadFields, ExecutionPayload, ExecutionPayloadSidecar, ExecutionPayloadV1,
39 ExecutionPayloadV2, ExecutionPayloadV3, PraguePayloadFields,
40};
41use snap::{read::FrameDecoder, write::FrameEncoder};
42use ssz::Decode;
43use std::io::{Read, Write};
44
45const MAX_DECOMPRESSED_SIGNED_BEACON_BLOCK_BYTES: usize = 256 * 1024 * 1024; const MAX_DECOMPRESSED_BEACON_STATE_BYTES: usize = 2 * 1024 * 1024 * 1024; fn decompress_snappy_bounded(
52 compressed: &[u8],
53 max_decompressed_bytes: usize,
54 what: &str,
55) -> Result<Vec<u8>, E2sError> {
56 let mut decoder = FrameDecoder::new(compressed).take(max_decompressed_bytes as u64);
57 let mut decompressed = Vec::new();
58
59 Read::read_to_end(&mut decoder, &mut decompressed)
60 .map_err(|e| E2sError::SnappyDecompression(format!("Failed to decompress {what}: {e}")))?;
61
62 if decompressed.len() >= max_decompressed_bytes {
63 return Err(E2sError::SnappyDecompression(format!(
64 "Failed to decompress {what}: decompressed data exceeded limit of {max_decompressed_bytes} bytes"
65 )));
66 }
67
68 Ok(decompressed)
69}
70
71pub const COMPRESSED_SIGNED_BEACON_BLOCK: [u8; 2] = [0x01, 0x00];
73
74pub const COMPRESSED_BEACON_STATE: [u8; 2] = [0x02, 0x00];
76
77#[derive(Debug, Clone)]
81pub struct CompressedSignedBeaconBlock {
82 pub data: Vec<u8>,
84}
85
86impl CompressedSignedBeaconBlock {
87 pub const fn new(data: Vec<u8>) -> Self {
89 Self { data }
90 }
91
92 pub fn from_ssz(ssz_data: &[u8]) -> Result<Self, E2sError> {
94 let mut compressed = Vec::new();
95 {
96 let mut encoder = FrameEncoder::new(&mut compressed);
97
98 Write::write_all(&mut encoder, ssz_data).map_err(|e| {
99 E2sError::SnappyCompression(format!("Failed to compress signed beacon block: {e}"))
100 })?;
101
102 encoder.flush().map_err(|e| {
103 E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
104 })?;
105 }
106 Ok(Self { data: compressed })
107 }
108
109 pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
111 decompress_snappy_bounded(
112 self.data.as_slice(),
113 MAX_DECOMPRESSED_SIGNED_BEACON_BLOCK_BYTES,
114 "signed beacon block",
115 )
116 }
117
118 pub fn decode_execution_block<T: Decodable2718>(&self) -> Result<Option<Block<T>>, E2sError> {
125 let ssz = self.decompress()?;
126
127 if let Ok(beacon) = SignedBeaconBlockElectra::<ExecutionPayloadV3>::from_ssz_bytes(&ssz) {
128 let sidecar = ExecutionPayloadSidecar::v4(
129 CancunPayloadFields {
130 parent_beacon_block_root: beacon.message.parent_root,
131 versioned_hashes: Vec::new(),
132 },
133 PraguePayloadFields::new(beacon.message.body.execution_requests.to_requests()),
134 );
135 let payload = ExecutionPayload::V3(beacon.message.body.execution_payload);
136 return Ok(Some(payload.try_into_block_with_sidecar(&sidecar)?));
137 }
138
139 if let Ok(beacon) = SignedBeaconBlockDeneb::<ExecutionPayloadV3>::from_ssz_bytes(&ssz) {
140 let sidecar = ExecutionPayloadSidecar::v3(CancunPayloadFields {
141 parent_beacon_block_root: beacon.message.parent_root,
142 versioned_hashes: Vec::new(),
143 });
144 let payload = ExecutionPayload::V3(beacon.message.body.execution_payload);
145 return Ok(Some(payload.try_into_block_with_sidecar(&sidecar)?));
146 }
147
148 if let Ok(beacon) = SignedBeaconBlockCapella::<ExecutionPayloadV2>::from_ssz_bytes(&ssz) {
149 let payload = ExecutionPayload::V2(beacon.message.body.execution_payload);
150 return Ok(Some(payload.try_into_block()?));
151 }
152
153 if let Ok(beacon) = SignedBeaconBlockBellatrix::<ExecutionPayloadV1>::from_ssz_bytes(&ssz) {
154 let payload = ExecutionPayload::V1(beacon.message.body.execution_payload);
155 return Ok(Some(payload.try_into_block()?));
156 }
157
158 if SignedBeaconBlockPhase0::from_ssz_bytes(&ssz).is_ok() ||
161 SignedBeaconBlockAltair::from_ssz_bytes(&ssz).is_ok()
162 {
163 return Ok(None);
164 }
165
166 Err(E2sError::Ssz(format!(
167 "consensus block ({} bytes) is not a valid SignedBeaconBlock of any known fork",
168 ssz.len()
169 )))
170 }
171
172 pub fn to_entry(&self) -> Entry {
174 Entry::new(COMPRESSED_SIGNED_BEACON_BLOCK, self.data.clone())
175 }
176
177 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
179 if entry.entry_type != COMPRESSED_SIGNED_BEACON_BLOCK {
180 return Err(E2sError::Ssz(format!(
181 "Invalid entry type for CompressedSignedBeaconBlock: expected {:02x}{:02x}, got {:02x}{:02x}",
182 COMPRESSED_SIGNED_BEACON_BLOCK[0],
183 COMPRESSED_SIGNED_BEACON_BLOCK[1],
184 entry.entry_type[0],
185 entry.entry_type[1]
186 )));
187 }
188
189 Ok(Self { data: entry.data.clone() })
190 }
191}
192
193#[derive(Debug, Clone)]
197pub struct CompressedBeaconState {
198 pub data: Vec<u8>,
200}
201
202impl CompressedBeaconState {
203 pub const fn new(data: Vec<u8>) -> Self {
205 Self { data }
206 }
207
208 pub fn from_ssz(ssz_data: &[u8]) -> Result<Self, E2sError> {
210 let mut compressed = Vec::new();
211 {
212 let mut encoder = FrameEncoder::new(&mut compressed);
213
214 Write::write_all(&mut encoder, ssz_data).map_err(|e| {
215 E2sError::SnappyCompression(format!("Failed to compress beacon state: {e}"))
216 })?;
217
218 encoder.flush().map_err(|e| {
219 E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
220 })?;
221 }
222 Ok(Self { data: compressed })
223 }
224
225 pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
227 decompress_snappy_bounded(
228 self.data.as_slice(),
229 MAX_DECOMPRESSED_BEACON_STATE_BYTES,
230 "beacon state",
231 )
232 }
233
234 pub fn to_entry(&self) -> Entry {
236 Entry::new(COMPRESSED_BEACON_STATE, self.data.clone())
237 }
238
239 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
241 if entry.entry_type != COMPRESSED_BEACON_STATE {
242 return Err(E2sError::Ssz(format!(
243 "Invalid entry type for CompressedBeaconState: expected {:02x}{:02x}, got {:02x}{:02x}",
244 COMPRESSED_BEACON_STATE[0],
245 COMPRESSED_BEACON_STATE[1],
246 entry.entry_type[0],
247 entry.entry_type[1]
248 )));
249 }
250
251 Ok(Self { data: entry.data.clone() })
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use alloy_primitives::B256;
259 use alloy_rpc_types_beacon::{
260 block::{BeaconBlock, BeaconBlockBodyPhase0, Eth1Data, SignedBeaconBlock},
261 BlsSignature,
262 };
263 use reth_ethereum_primitives::TransactionSigned;
264 use ssz::Encode;
265
266 #[test]
267 fn test_signed_beacon_block_compression_roundtrip() {
268 let ssz_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
269
270 let compressed_block = CompressedSignedBeaconBlock::from_ssz(&ssz_data).unwrap();
271 let decompressed = compressed_block.decompress().unwrap();
272
273 assert_eq!(decompressed, ssz_data);
274 }
275
276 #[test]
277 fn test_beacon_state_compression_roundtrip() {
278 let ssz_data = vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
279
280 let compressed_state = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
281 let decompressed = compressed_state.decompress().unwrap();
282
283 assert_eq!(decompressed, ssz_data);
284 }
285
286 #[test]
287 fn test_entry_conversion_signed_beacon_block() {
288 let ssz_data = vec![1, 2, 3, 4, 5];
289 let compressed_block = CompressedSignedBeaconBlock::from_ssz(&ssz_data).unwrap();
290
291 let entry = compressed_block.to_entry();
292 assert_eq!(entry.entry_type, COMPRESSED_SIGNED_BEACON_BLOCK);
293
294 let recovered = CompressedSignedBeaconBlock::from_entry(&entry).unwrap();
295 let recovered_ssz = recovered.decompress().unwrap();
296
297 assert_eq!(recovered_ssz, ssz_data);
298 }
299
300 #[test]
301 fn test_entry_conversion_beacon_state() {
302 let ssz_data = vec![5, 4, 3, 2, 1];
303 let compressed_state = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
304
305 let entry = compressed_state.to_entry();
306 assert_eq!(entry.entry_type, COMPRESSED_BEACON_STATE);
307
308 let recovered = CompressedBeaconState::from_entry(&entry).unwrap();
309 let recovered_ssz = recovered.decompress().unwrap();
310
311 assert_eq!(recovered_ssz, ssz_data);
312 }
313
314 #[test]
315 fn test_invalid_entry_type() {
316 let invalid_entry = Entry::new([0xFF, 0xFF], vec![1, 2, 3]);
317
318 let result = CompressedSignedBeaconBlock::from_entry(&invalid_entry);
319 assert!(result.is_err());
320
321 let result = CompressedBeaconState::from_entry(&invalid_entry);
322 assert!(result.is_err());
323 }
324
325 #[test]
326 fn test_bounded_decompression_rejects_oversized_output() {
327 let ssz_data = vec![42u8; 1024];
328 let compressed = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
329
330 let err =
331 decompress_snappy_bounded(compressed.data.as_slice(), 100, "beacon state").unwrap_err();
332
333 assert!(format!("{err:?}").contains("exceeded limit"));
334 }
335
336 #[test]
337 fn decode_execution_block_skips_genuine_pre_merge() {
338 let block = SignedBeaconBlock {
340 message: BeaconBlock {
341 slot: 0,
342 proposer_index: 0,
343 parent_root: B256::ZERO,
344 state_root: B256::ZERO,
345 body: BeaconBlockBodyPhase0 {
346 randao_reveal: BlsSignature::ZERO,
347 eth1_data: Eth1Data {
348 deposit_root: B256::ZERO,
349 deposit_count: 0,
350 block_hash: B256::ZERO,
351 },
352 graffiti: B256::ZERO,
353 proposer_slashings: vec![],
354 attester_slashings: vec![],
355 attestations: vec![],
356 deposits: vec![],
357 voluntary_exits: vec![],
358 },
359 },
360 signature: BlsSignature::ZERO,
361 };
362 let compressed = CompressedSignedBeaconBlock::from_ssz(&block.as_ssz_bytes()).unwrap();
363 assert!(compressed.decode_execution_block::<TransactionSigned>().unwrap().is_none());
364 }
365
366 #[test]
367 fn decode_execution_block_errors_on_malformed() {
368 for bytes in [vec![], vec![0u8; 8]] {
371 let compressed = CompressedSignedBeaconBlock::from_ssz(&bytes).unwrap();
372 assert!(compressed.decode_execution_block::<TransactionSigned>().is_err());
373 }
374 }
375}