Skip to main content

reth_era/era/types/
consensus.rs

1//! Consensus types for Era post-merge history files
2//!
3//! # Decoding
4//!
5//! This crate only handles compression/decompression.
6//! To decode the SSZ data into concrete beacon types, use the [Lighthouse `types`](https://github.com/sigp/lighthouse/tree/stable/consensus/types)
7//! crate or another SSZ-compatible library.
8//!
9//! # Examples
10//!
11//! ## Decoding a [`CompressedBeaconState`]
12//!
13//! ```ignore
14//! use types::{BeaconState, ChainSpec, MainnetEthSpec};
15//! use reth_era::era::types::consensus::CompressedBeaconState;
16//!
17//! fn decode_state(
18//!     compressed_state: &CompressedBeaconState,
19//! ) -> Result<(), Box<dyn std::error::Error>> {
20//!     let spec = ChainSpec::mainnet();
21//!
22//!     // Decompress to get SSZ bytes
23//!     let ssz_bytes = compressed_state.decompress()?;
24//!
25//!     // Decode with fork-aware method, chainSpec determines fork from slot in SSZ
26//!     let state = BeaconState::<MainnetEthSpec>::from_ssz_bytes(&ssz_bytes, &spec)
27//!         .map_err(|e| format!("{:?}", e))?;
28//!
29//!     println!("State slot: {}", state.slot());
30//!     println!("Fork: {:?}", state.fork_name_unchecked());
31//!     println!("Validators: {}", state.validators().len());
32//!     println!("Finalized checkpoint: {:?}", state.finalized_checkpoint());
33//!     Ok(())
34//! }
35//! ```
36//!
37//! ## Decoding a [`CompressedSignedBeaconBlock`]
38//!
39//! ```ignore
40//! use consensus_types::{ForkName, ForkVersionDecode, MainnetEthSpec, SignedBeaconBlock};
41//! use reth_era::era::types::consensus::CompressedSignedBeaconBlock;
42//!
43//! // Decode using fork-aware decoding, fork must be known beforehand
44//! fn decode_block(
45//!     compressed: &CompressedSignedBeaconBlock,
46//!     fork: ForkName,
47//! ) -> Result<(), Box<dyn std::error::Error>> {
48//!     // Decompress to get SSZ bytes
49//!     let ssz_bytes = compressed.decompress()?;
50//!
51//!     let block = SignedBeaconBlock::<MainnetEthSpec>::from_ssz_bytes_by_fork(&ssz_bytes, fork)
52//!         .map_err(|e| format!("{:?}", e))?;
53//!
54//!     println!("Block slot: {}", block.message().slot());
55//!     println!("Proposer index: {}", block.message().proposer_index());
56//!     println!("Parent root: {:?}", block.message().parent_root());
57//!     println!("State root: {:?}", block.message().state_root());
58//!
59//!     Ok(())
60//! }
61//! ```
62use crate::e2s::{error::E2sError, types::Entry};
63use snap::{read::FrameDecoder, write::FrameEncoder};
64use std::io::{Read, Write};
65
66/// Maximum allowed decompressed size for a signed beacon block SSZ payload.
67const MAX_DECOMPRESSED_SIGNED_BEACON_BLOCK_BYTES: usize = 256 * 1024 * 1024; // 256 MiB
68
69/// Maximum allowed decompressed size for a beacon state SSZ payload.
70const MAX_DECOMPRESSED_BEACON_STATE_BYTES: usize = 2 * 1024 * 1024 * 1024; // 2 GiB
71
72fn decompress_snappy_bounded(
73    compressed: &[u8],
74    max_decompressed_bytes: usize,
75    what: &str,
76) -> Result<Vec<u8>, E2sError> {
77    let mut decoder = FrameDecoder::new(compressed).take(max_decompressed_bytes as u64);
78    let mut decompressed = Vec::new();
79
80    Read::read_to_end(&mut decoder, &mut decompressed)
81        .map_err(|e| E2sError::SnappyDecompression(format!("Failed to decompress {what}: {e}")))?;
82
83    if decompressed.len() >= max_decompressed_bytes {
84        return Err(E2sError::SnappyDecompression(format!(
85            "Failed to decompress {what}: decompressed data exceeded limit of {max_decompressed_bytes} bytes"
86        )));
87    }
88
89    Ok(decompressed)
90}
91
92/// `CompressedSignedBeaconBlock` record type: [0x01, 0x00]
93pub const COMPRESSED_SIGNED_BEACON_BLOCK: [u8; 2] = [0x01, 0x00];
94
95/// `CompressedBeaconState` record type: [0x02, 0x00]
96pub const COMPRESSED_BEACON_STATE: [u8; 2] = [0x02, 0x00];
97
98/// Compressed signed beacon block
99///
100/// See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md#compressedsignedbeaconblock>.
101#[derive(Debug, Clone)]
102pub struct CompressedSignedBeaconBlock {
103    /// Snappy-compressed ssz-encoded `SignedBeaconBlock`
104    pub data: Vec<u8>,
105}
106
107impl CompressedSignedBeaconBlock {
108    /// Create a new [`CompressedSignedBeaconBlock`] from compressed data
109    pub const fn new(data: Vec<u8>) -> Self {
110        Self { data }
111    }
112
113    /// Create from ssz-encoded block by compressing it with snappy
114    pub fn from_ssz(ssz_data: &[u8]) -> Result<Self, E2sError> {
115        let mut compressed = Vec::new();
116        {
117            let mut encoder = FrameEncoder::new(&mut compressed);
118
119            Write::write_all(&mut encoder, ssz_data).map_err(|e| {
120                E2sError::SnappyCompression(format!("Failed to compress signed beacon block: {e}"))
121            })?;
122
123            encoder.flush().map_err(|e| {
124                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
125            })?;
126        }
127        Ok(Self { data: compressed })
128    }
129
130    /// Decompress to get the original ssz-encoded signed beacon block
131    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
132        decompress_snappy_bounded(
133            self.data.as_slice(),
134            MAX_DECOMPRESSED_SIGNED_BEACON_BLOCK_BYTES,
135            "signed beacon block",
136        )
137    }
138
139    /// Convert to an [`Entry`]
140    pub fn to_entry(&self) -> Entry {
141        Entry::new(COMPRESSED_SIGNED_BEACON_BLOCK, 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_SIGNED_BEACON_BLOCK {
147            return Err(E2sError::Ssz(format!(
148                "Invalid entry type for CompressedSignedBeaconBlock: expected {:02x}{:02x}, got {:02x}{:02x}",
149                COMPRESSED_SIGNED_BEACON_BLOCK[0],
150                COMPRESSED_SIGNED_BEACON_BLOCK[1],
151                entry.entry_type[0],
152                entry.entry_type[1]
153            )));
154        }
155
156        Ok(Self { data: entry.data.clone() })
157    }
158}
159
160/// Compressed beacon state
161///
162/// See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md#compressedbeaconstate>.
163#[derive(Debug, Clone)]
164pub struct CompressedBeaconState {
165    /// Snappy-compressed ssz-encoded `BeaconState`
166    pub data: Vec<u8>,
167}
168
169impl CompressedBeaconState {
170    /// Create a new [`CompressedBeaconState`] from compressed data
171    pub const fn new(data: Vec<u8>) -> Self {
172        Self { data }
173    }
174
175    /// Compress with snappy from ssz-encoded state
176    pub fn from_ssz(ssz_data: &[u8]) -> Result<Self, E2sError> {
177        let mut compressed = Vec::new();
178        {
179            let mut encoder = FrameEncoder::new(&mut compressed);
180
181            Write::write_all(&mut encoder, ssz_data).map_err(|e| {
182                E2sError::SnappyCompression(format!("Failed to compress beacon state: {e}"))
183            })?;
184
185            encoder.flush().map_err(|e| {
186                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
187            })?;
188        }
189        Ok(Self { data: compressed })
190    }
191
192    /// Decompress to get the original ssz-encoded beacon state
193    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
194        decompress_snappy_bounded(
195            self.data.as_slice(),
196            MAX_DECOMPRESSED_BEACON_STATE_BYTES,
197            "beacon state",
198        )
199    }
200
201    /// Convert to an [`Entry`]
202    pub fn to_entry(&self) -> Entry {
203        Entry::new(COMPRESSED_BEACON_STATE, self.data.clone())
204    }
205
206    /// Create from an [`Entry`]
207    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
208        if entry.entry_type != COMPRESSED_BEACON_STATE {
209            return Err(E2sError::Ssz(format!(
210                "Invalid entry type for CompressedBeaconState: expected {:02x}{:02x}, got {:02x}{:02x}",
211                COMPRESSED_BEACON_STATE[0],
212                COMPRESSED_BEACON_STATE[1],
213                entry.entry_type[0],
214                entry.entry_type[1]
215            )));
216        }
217
218        Ok(Self { data: entry.data.clone() })
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_signed_beacon_block_compression_roundtrip() {
228        let ssz_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
229
230        let compressed_block = CompressedSignedBeaconBlock::from_ssz(&ssz_data).unwrap();
231        let decompressed = compressed_block.decompress().unwrap();
232
233        assert_eq!(decompressed, ssz_data);
234    }
235
236    #[test]
237    fn test_beacon_state_compression_roundtrip() {
238        let ssz_data = vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
239
240        let compressed_state = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
241        let decompressed = compressed_state.decompress().unwrap();
242
243        assert_eq!(decompressed, ssz_data);
244    }
245
246    #[test]
247    fn test_entry_conversion_signed_beacon_block() {
248        let ssz_data = vec![1, 2, 3, 4, 5];
249        let compressed_block = CompressedSignedBeaconBlock::from_ssz(&ssz_data).unwrap();
250
251        let entry = compressed_block.to_entry();
252        assert_eq!(entry.entry_type, COMPRESSED_SIGNED_BEACON_BLOCK);
253
254        let recovered = CompressedSignedBeaconBlock::from_entry(&entry).unwrap();
255        let recovered_ssz = recovered.decompress().unwrap();
256
257        assert_eq!(recovered_ssz, ssz_data);
258    }
259
260    #[test]
261    fn test_entry_conversion_beacon_state() {
262        let ssz_data = vec![5, 4, 3, 2, 1];
263        let compressed_state = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
264
265        let entry = compressed_state.to_entry();
266        assert_eq!(entry.entry_type, COMPRESSED_BEACON_STATE);
267
268        let recovered = CompressedBeaconState::from_entry(&entry).unwrap();
269        let recovered_ssz = recovered.decompress().unwrap();
270
271        assert_eq!(recovered_ssz, ssz_data);
272    }
273
274    #[test]
275    fn test_invalid_entry_type() {
276        let invalid_entry = Entry::new([0xFF, 0xFF], vec![1, 2, 3]);
277
278        let result = CompressedSignedBeaconBlock::from_entry(&invalid_entry);
279        assert!(result.is_err());
280
281        let result = CompressedBeaconState::from_entry(&invalid_entry);
282        assert!(result.is_err());
283    }
284
285    #[test]
286    fn test_bounded_decompression_rejects_oversized_output() {
287        let ssz_data = vec![42u8; 1024];
288        let compressed = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
289
290        let err =
291            decompress_snappy_bounded(compressed.data.as_slice(), 100, "beacon state").unwrap_err();
292
293        assert!(format!("{err:?}").contains("exceeded limit"));
294    }
295}