reth_era/era/types/
group.rs

1//! Era types for `.era` file content
2//!
3//! See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md>
4
5use crate::{
6    common::file_ops::EraFileId,
7    e2s::types::{Entry, IndexEntry, SLOT_INDEX},
8    era::types::consensus::{CompressedBeaconState, CompressedSignedBeaconBlock},
9};
10
11/// Number of slots per historical root in ERA files
12pub const SLOTS_PER_HISTORICAL_ROOT: u64 = 8192;
13
14/// Era file content group
15///
16/// Format: `Version | block* | era-state | other-entries* | slot-index(block)? | slot-index(state)`
17/// See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md#structure>
18#[derive(Debug)]
19pub struct EraGroup {
20    /// Group including all blocks leading up to the era transition in slot order
21    pub blocks: Vec<CompressedSignedBeaconBlock>,
22
23    /// State in the era transition slot
24    pub era_state: CompressedBeaconState,
25
26    /// Other entries that don't fit into standard categories
27    pub other_entries: Vec<Entry>,
28
29    /// Block slot index, omitted for genesis era
30    pub slot_index: Option<SlotIndex>,
31
32    /// State slot index
33    pub state_slot_index: SlotIndex,
34}
35
36impl EraGroup {
37    /// Create a new era group
38    pub const fn new(
39        blocks: Vec<CompressedSignedBeaconBlock>,
40        era_state: CompressedBeaconState,
41        state_slot_index: SlotIndex,
42    ) -> Self {
43        Self { blocks, era_state, other_entries: Vec::new(), slot_index: None, state_slot_index }
44    }
45
46    /// Create a new era group with block slot index
47    pub const fn with_block_index(
48        blocks: Vec<CompressedSignedBeaconBlock>,
49        era_state: CompressedBeaconState,
50        slot_index: SlotIndex,
51        state_slot_index: SlotIndex,
52    ) -> Self {
53        Self {
54            blocks,
55            era_state,
56            other_entries: Vec::new(),
57            slot_index: Some(slot_index),
58            state_slot_index,
59        }
60    }
61
62    /// Check if this is a genesis era - no blocks yet
63    pub const fn is_genesis(&self) -> bool {
64        self.blocks.is_empty() && self.slot_index.is_none()
65    }
66
67    /// Add another entry to this group
68    pub fn add_entry(&mut self, entry: Entry) {
69        self.other_entries.push(entry);
70    }
71
72    /// Get the starting slot and slot count.
73    pub const fn slot_range(&self) -> (u64, u32) {
74        if let Some(ref block_index) = self.slot_index {
75            // Non-genesis era: use block slot index
76            (block_index.starting_slot, block_index.slot_count() as u32)
77        } else {
78            // Genesis era: use state slot index, it should be slot 0
79            // Genesis has only the genesis state, no blocks
80            (self.state_slot_index.starting_slot, 0)
81        }
82    }
83
84    /// Get the starting slot number
85    pub const fn starting_slot(&self) -> u64 {
86        self.slot_range().0
87    }
88
89    /// Get the number of slots
90    pub const fn slot_count(&self) -> u32 {
91        self.slot_range().1
92    }
93}
94
95/// [`SlotIndex`] records store offsets to data at specific slots
96/// from the beginning of the index record to the beginning of the corresponding data.
97///
98/// Format: `starting-slot | index | index | index ... | count`
99///
100/// See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md#slotindex>.
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct SlotIndex {
103    /// Starting slot number
104    pub starting_slot: u64,
105
106    /// Offsets to data at each slot
107    /// 0 indicates no data for that slot
108    pub offsets: Vec<i64>,
109}
110
111impl SlotIndex {
112    /// Create a new slot index
113    pub const fn new(starting_slot: u64, offsets: Vec<i64>) -> Self {
114        Self { starting_slot, offsets }
115    }
116
117    /// Get the number of slots covered by this index
118    pub const fn slot_count(&self) -> usize {
119        self.offsets.len()
120    }
121
122    /// Get the offset for a specific slot
123    pub fn get_offset(&self, slot_index: usize) -> Option<i64> {
124        self.offsets.get(slot_index).copied()
125    }
126
127    /// Check if a slot has data - non-zero offset
128    pub fn has_data_at_slot(&self, slot_index: usize) -> bool {
129        self.get_offset(slot_index).is_some_and(|offset| offset != 0)
130    }
131}
132
133impl IndexEntry for SlotIndex {
134    fn new(starting_number: u64, offsets: Vec<i64>) -> Self {
135        Self { starting_slot: starting_number, offsets }
136    }
137
138    fn entry_type() -> [u8; 2] {
139        SLOT_INDEX
140    }
141
142    fn starting_number(&self) -> u64 {
143        self.starting_slot
144    }
145
146    fn offsets(&self) -> &[i64] {
147        &self.offsets
148    }
149}
150
151/// Era file identifier
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct EraId {
154    /// Network configuration name
155    pub network_name: String,
156
157    /// First slot number in file
158    pub start_slot: u64,
159
160    /// Number of slots in the file
161    pub slot_count: u32,
162
163    /// Optional hash identifier for this file
164    /// First 4 bytes of the last historical root in the last state in the era file
165    pub hash: Option<[u8; 4]>,
166}
167
168impl EraId {
169    /// Create a new [`EraId`]
170    pub fn new(network_name: impl Into<String>, start_slot: u64, slot_count: u32) -> Self {
171        Self { network_name: network_name.into(), start_slot, slot_count, hash: None }
172    }
173
174    /// Add a hash identifier to  [`EraId`]
175    pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
176        self.hash = Some(hash);
177        self
178    }
179
180    /// Calculate which era number the file starts at
181    pub const fn era_number(&self) -> u64 {
182        self.start_slot / SLOTS_PER_HISTORICAL_ROOT
183    }
184
185    // Helper function to calculate the number of eras per era1 file,
186    // If the user can decide how many blocks per era1 file there are, we need to calculate it.
187    // Most of the time it should be 1, but it can never be more than 2 eras per file
188    // as there is a maximum of 8192 blocks per era1 file.
189    const fn calculate_era_count(&self) -> u64 {
190        if self.slot_count == 0 {
191            return 0;
192        }
193
194        let first_era = self.era_number();
195
196        // Calculate the actual last slot number in the range
197        let last_slot = self.start_slot + self.slot_count as u64 - 1;
198        // Find which era the last block belongs to
199        let last_era = last_slot / SLOTS_PER_HISTORICAL_ROOT;
200        // Count how many eras we span
201        last_era - first_era + 1
202    }
203}
204
205impl EraFileId for EraId {
206    fn network_name(&self) -> &str {
207        &self.network_name
208    }
209
210    fn start_number(&self) -> u64 {
211        self.start_slot
212    }
213
214    fn count(&self) -> u32 {
215        self.slot_count
216    }
217    /// Convert to file name following the era file naming:
218    /// `<config-name>-<era-number>-<era-count>-<short-historical-root>.era`
219    /// <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md#file-name>
220    /// See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md>
221    fn to_file_name(&self) -> String {
222        let era_number = self.era_number();
223        let era_count = self.calculate_era_count();
224
225        if let Some(hash) = self.hash {
226            format!(
227                "{}-{:05}-{:05}-{:02x}{:02x}{:02x}{:02x}.era",
228                self.network_name, era_number, era_count, hash[0], hash[1], hash[2], hash[3]
229            )
230        } else {
231            // era spec format with placeholder hash when no hash available
232            // Format: `<config-name>-<era-number>-<era-count>-00000000.era`
233            format!("{}-{:05}-{:05}-00000000.era", self.network_name, era_number, era_count)
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::{
242        e2s::types::{Entry, IndexEntry},
243        test_utils::{create_beacon_block, create_beacon_state},
244    };
245
246    #[test]
247    fn test_slot_index_roundtrip() {
248        let starting_slot = 1000;
249        let offsets = vec![100, 200, 300, 400, 500];
250
251        let slot_index = SlotIndex::new(starting_slot, offsets.clone());
252
253        let entry = slot_index.to_entry();
254
255        // Validate entry type
256        assert_eq!(entry.entry_type, SLOT_INDEX);
257
258        // Convert back to slot index
259        let recovered = SlotIndex::from_entry(&entry).unwrap();
260
261        // Verify fields match
262        assert_eq!(recovered.starting_slot, starting_slot);
263        assert_eq!(recovered.offsets, offsets);
264    }
265    #[test]
266    fn test_slot_index_basic_operations() {
267        let starting_slot = 2000;
268        let offsets = vec![100, 200, 300];
269
270        let slot_index = SlotIndex::new(starting_slot, offsets);
271
272        assert_eq!(slot_index.slot_count(), 3);
273        assert_eq!(slot_index.starting_slot, 2000);
274    }
275
276    #[test]
277    fn test_slot_index_empty_slots() {
278        let starting_slot = 1000;
279        let offsets = vec![100, 0, 300, 0, 500];
280
281        let slot_index = SlotIndex::new(starting_slot, offsets);
282
283        // Test that empty slots return false for has_data_at_slot
284        // slot 1000: offset 100
285        assert!(slot_index.has_data_at_slot(0));
286        // slot 1001: offset 0 - empty
287        assert!(!slot_index.has_data_at_slot(1));
288        // slot 1002: offset 300
289        assert!(slot_index.has_data_at_slot(2));
290        // slot 1003: offset 0 - empty
291        assert!(!slot_index.has_data_at_slot(3));
292        // slot 1004: offset 500
293        assert!(slot_index.has_data_at_slot(4));
294    }
295
296    #[test]
297    fn test_era_group_basic_construction() {
298        let blocks =
299            vec![create_beacon_block(10), create_beacon_block(15), create_beacon_block(20)];
300        let era_state = create_beacon_state(50);
301        let state_slot_index = SlotIndex::new(1000, vec![100, 200, 300]);
302
303        let era_group = EraGroup::new(blocks, era_state, state_slot_index);
304
305        // Verify initial state
306        assert_eq!(era_group.blocks.len(), 3);
307        assert_eq!(era_group.other_entries.len(), 0);
308        assert_eq!(era_group.slot_index, None);
309        assert_eq!(era_group.state_slot_index.starting_slot, 1000);
310        assert_eq!(era_group.state_slot_index.offsets, vec![100, 200, 300]);
311    }
312
313    #[test]
314    fn test_era_group_with_block_index() {
315        let blocks = vec![create_beacon_block(10), create_beacon_block(15)];
316        let era_state = create_beacon_state(50);
317        let block_slot_index = SlotIndex::new(500, vec![50, 100]);
318        let state_slot_index = SlotIndex::new(1000, vec![200, 300]);
319
320        let era_group =
321            EraGroup::with_block_index(blocks, era_state, block_slot_index, state_slot_index);
322
323        // Verify state with block index
324        assert_eq!(era_group.blocks.len(), 2);
325        assert_eq!(era_group.other_entries.len(), 0);
326        assert!(era_group.slot_index.is_some());
327
328        let block_index = era_group.slot_index.as_ref().unwrap();
329        assert_eq!(block_index.starting_slot, 500);
330        assert_eq!(block_index.offsets, vec![50, 100]);
331
332        assert_eq!(era_group.state_slot_index.starting_slot, 1000);
333        assert_eq!(era_group.state_slot_index.offsets, vec![200, 300]);
334    }
335
336    #[test]
337    fn test_era_group_genesis_check() {
338        // Genesis era - no blocks, no block slot index
339        let era_state = create_beacon_state(50);
340        let state_slot_index = SlotIndex::new(0, vec![100]);
341
342        let genesis_era = EraGroup::new(vec![], era_state, state_slot_index);
343        assert!(genesis_era.is_genesis());
344
345        // Non-genesis era - has blocks
346        let blocks = vec![create_beacon_block(10)];
347        let era_state = create_beacon_state(50);
348        let state_slot_index = SlotIndex::new(1000, vec![100]);
349
350        let normal_era = EraGroup::new(blocks, era_state, state_slot_index);
351        assert!(!normal_era.is_genesis());
352
353        // Non-genesis era - has block slot index
354        let era_state = create_beacon_state(50);
355        let block_slot_index = SlotIndex::new(500, vec![50]);
356        let state_slot_index = SlotIndex::new(1000, vec![100]);
357
358        let era_with_index =
359            EraGroup::with_block_index(vec![], era_state, block_slot_index, state_slot_index);
360        assert!(!era_with_index.is_genesis());
361    }
362
363    #[test]
364    fn test_era_group_add_entries() {
365        let blocks = vec![create_beacon_block(10)];
366        let era_state = create_beacon_state(50);
367        let state_slot_index = SlotIndex::new(1000, vec![100]);
368
369        // Create and verify group
370        let mut era_group = EraGroup::new(blocks, era_state, state_slot_index);
371        assert_eq!(era_group.other_entries.len(), 0);
372
373        // Create custom entries with different types
374        let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
375        let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
376
377        // Add those entries
378        era_group.add_entry(entry1);
379        era_group.add_entry(entry2);
380
381        // Verify entries were added correctly
382        assert_eq!(era_group.other_entries.len(), 2);
383        assert_eq!(era_group.other_entries[0].entry_type, [0x01, 0x01]);
384        assert_eq!(era_group.other_entries[0].data, vec![1, 2, 3, 4]);
385        assert_eq!(era_group.other_entries[1].entry_type, [0x02, 0x02]);
386        assert_eq!(era_group.other_entries[1].data, vec![5, 6, 7, 8]);
387    }
388
389    #[test]
390    fn test_index_with_negative_offset() {
391        let mut data = Vec::new();
392        data.extend_from_slice(&0u64.to_le_bytes());
393        data.extend_from_slice(&(-1024i64).to_le_bytes());
394        data.extend_from_slice(&0i64.to_le_bytes());
395        data.extend_from_slice(&2i64.to_le_bytes());
396
397        let entry = Entry::new(SLOT_INDEX, data);
398        let index = SlotIndex::from_entry(&entry).unwrap();
399        let parsed_offset = index.offsets[0];
400        assert_eq!(parsed_offset, -1024);
401    }
402}