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//! ```
62
63use crate::e2s::{error::E2sError, types::Entry};
64use snap::{read::FrameDecoder, write::FrameEncoder};
65use std::io::{Read, Write};
66
67/// `CompressedSignedBeaconBlock` record type: [0x01, 0x00]
68pub const COMPRESSED_SIGNED_BEACON_BLOCK: [u8; 2] = [0x01, 0x00];
69
70/// `CompressedBeaconState` record type: [0x02, 0x00]
71pub const COMPRESSED_BEACON_STATE: [u8; 2] = [0x02, 0x00];
72
73/// Compressed signed beacon block
74///
75/// See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md#compressedsignedbeaconblock>.
76#[derive(Debug, Clone)]
77pub struct CompressedSignedBeaconBlock {
78    /// Snappy-compressed ssz-encoded `SignedBeaconBlock`
79    pub data: Vec<u8>,
80}
81
82impl CompressedSignedBeaconBlock {
83    /// Create a new [`CompressedSignedBeaconBlock`] from compressed data
84    pub const fn new(data: Vec<u8>) -> Self {
85        Self { data }
86    }
87
88    /// Create from ssz-encoded block by compressing it with snappy
89    pub fn from_ssz(ssz_data: &[u8]) -> Result<Self, E2sError> {
90        let mut compressed = Vec::new();
91        {
92            let mut encoder = FrameEncoder::new(&mut compressed);
93
94            Write::write_all(&mut encoder, ssz_data).map_err(|e| {
95                E2sError::SnappyCompression(format!("Failed to compress signed beacon block: {e}"))
96            })?;
97
98            encoder.flush().map_err(|e| {
99                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
100            })?;
101        }
102        Ok(Self { data: compressed })
103    }
104
105    /// Decompress to get the original ssz-encoded signed beacon block
106    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
107        let mut decoder = FrameDecoder::new(self.data.as_slice());
108        let mut decompressed = Vec::new();
109        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
110            E2sError::SnappyDecompression(format!("Failed to decompress signed beacon block: {e}"))
111        })?;
112
113        Ok(decompressed)
114    }
115
116    /// Convert to an [`Entry`]
117    pub fn to_entry(&self) -> Entry {
118        Entry::new(COMPRESSED_SIGNED_BEACON_BLOCK, self.data.clone())
119    }
120
121    /// Create from an [`Entry`]
122    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
123        if entry.entry_type != COMPRESSED_SIGNED_BEACON_BLOCK {
124            return Err(E2sError::Ssz(format!(
125                "Invalid entry type for CompressedSignedBeaconBlock: expected {:02x}{:02x}, got {:02x}{:02x}",
126                COMPRESSED_SIGNED_BEACON_BLOCK[0],
127                COMPRESSED_SIGNED_BEACON_BLOCK[1],
128                entry.entry_type[0],
129                entry.entry_type[1]
130            )));
131        }
132
133        Ok(Self { data: entry.data.clone() })
134    }
135}
136
137/// Compressed beacon state
138///
139/// See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md#compressedbeaconstate>.
140#[derive(Debug, Clone)]
141pub struct CompressedBeaconState {
142    /// Snappy-compressed ssz-encoded `BeaconState`
143    pub data: Vec<u8>,
144}
145
146impl CompressedBeaconState {
147    /// Create a new [`CompressedBeaconState`] from compressed data
148    pub const fn new(data: Vec<u8>) -> Self {
149        Self { data }
150    }
151
152    /// Compress with snappy from ssz-encoded state
153    pub fn from_ssz(ssz_data: &[u8]) -> Result<Self, E2sError> {
154        let mut compressed = Vec::new();
155        {
156            let mut encoder = FrameEncoder::new(&mut compressed);
157
158            Write::write_all(&mut encoder, ssz_data).map_err(|e| {
159                E2sError::SnappyCompression(format!("Failed to compress beacon state: {e}"))
160            })?;
161
162            encoder.flush().map_err(|e| {
163                E2sError::SnappyCompression(format!("Failed to flush encoder: {e}"))
164            })?;
165        }
166        Ok(Self { data: compressed })
167    }
168
169    /// Decompress to get the original ssz-encoded beacon state
170    pub fn decompress(&self) -> Result<Vec<u8>, E2sError> {
171        let mut decoder = FrameDecoder::new(self.data.as_slice());
172        let mut decompressed = Vec::new();
173        Read::read_to_end(&mut decoder, &mut decompressed).map_err(|e| {
174            E2sError::SnappyDecompression(format!("Failed to decompress beacon state: {e}"))
175        })?;
176
177        Ok(decompressed)
178    }
179
180    /// Convert to an [`Entry`]
181    pub fn to_entry(&self) -> Entry {
182        Entry::new(COMPRESSED_BEACON_STATE, self.data.clone())
183    }
184
185    /// Create from an [`Entry`]
186    pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
187        if entry.entry_type != COMPRESSED_BEACON_STATE {
188            return Err(E2sError::Ssz(format!(
189                "Invalid entry type for CompressedBeaconState: expected {:02x}{:02x}, got {:02x}{:02x}",
190                COMPRESSED_BEACON_STATE[0],
191                COMPRESSED_BEACON_STATE[1],
192                entry.entry_type[0],
193                entry.entry_type[1]
194            )));
195        }
196
197        Ok(Self { data: entry.data.clone() })
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_signed_beacon_block_compression_roundtrip() {
207        let ssz_data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
208
209        let compressed_block = CompressedSignedBeaconBlock::from_ssz(&ssz_data).unwrap();
210        let decompressed = compressed_block.decompress().unwrap();
211
212        assert_eq!(decompressed, ssz_data);
213    }
214
215    #[test]
216    fn test_beacon_state_compression_roundtrip() {
217        let ssz_data = vec![10, 9, 8, 7, 6, 5, 4, 3, 2, 1];
218
219        let compressed_state = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
220        let decompressed = compressed_state.decompress().unwrap();
221
222        assert_eq!(decompressed, ssz_data);
223    }
224
225    #[test]
226    fn test_entry_conversion_signed_beacon_block() {
227        let ssz_data = vec![1, 2, 3, 4, 5];
228        let compressed_block = CompressedSignedBeaconBlock::from_ssz(&ssz_data).unwrap();
229
230        let entry = compressed_block.to_entry();
231        assert_eq!(entry.entry_type, COMPRESSED_SIGNED_BEACON_BLOCK);
232
233        let recovered = CompressedSignedBeaconBlock::from_entry(&entry).unwrap();
234        let recovered_ssz = recovered.decompress().unwrap();
235
236        assert_eq!(recovered_ssz, ssz_data);
237    }
238
239    #[test]
240    fn test_entry_conversion_beacon_state() {
241        let ssz_data = vec![5, 4, 3, 2, 1];
242        let compressed_state = CompressedBeaconState::from_ssz(&ssz_data).unwrap();
243
244        let entry = compressed_state.to_entry();
245        assert_eq!(entry.entry_type, COMPRESSED_BEACON_STATE);
246
247        let recovered = CompressedBeaconState::from_entry(&entry).unwrap();
248        let recovered_ssz = recovered.decompress().unwrap();
249
250        assert_eq!(recovered_ssz, ssz_data);
251    }
252
253    #[test]
254    fn test_invalid_entry_type() {
255        let invalid_entry = Entry::new([0xFF, 0xFF], vec![1, 2, 3]);
256
257        let result = CompressedSignedBeaconBlock::from_entry(&invalid_entry);
258        assert!(result.is_err());
259
260        let result = CompressedBeaconState::from_entry(&invalid_entry);
261        assert!(result.is_err());
262    }
263}