reth_era/
era_types.rs

1//! Era types for `.era` files
2//!
3//! See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md>
4
5use crate::{
6    consensus_types::{CompressedBeaconState, CompressedSignedBeaconBlock},
7    e2s_types::{Entry, IndexEntry, SLOT_INDEX},
8};
9
10/// Era file content group
11///
12/// Format: `Version | block* | era-state | other-entries* | slot-index(block)? | slot-index(state)`
13/// See also <https://github.com/eth-clients/e2store-format-specs/blob/main/formats/era.md#structure>
14#[derive(Debug)]
15pub struct EraGroup {
16    /// Group including all blocks leading up to the era transition in slot order
17    pub blocks: Vec<CompressedSignedBeaconBlock>,
18
19    /// State in the era transition slot
20    pub era_state: CompressedBeaconState,
21
22    /// Other entries that don't fit into standard categories
23    pub other_entries: Vec<Entry>,
24
25    /// Block slot index, omitted for genesis era
26    pub slot_index: Option<SlotIndex>,
27
28    /// State slot index
29    pub state_slot_index: SlotIndex,
30}
31
32impl EraGroup {
33    /// Create a new era group
34    pub const fn new(
35        blocks: Vec<CompressedSignedBeaconBlock>,
36        era_state: CompressedBeaconState,
37        state_slot_index: SlotIndex,
38    ) -> Self {
39        Self { blocks, era_state, other_entries: Vec::new(), slot_index: None, state_slot_index }
40    }
41
42    /// Create a new era group with block slot index
43    pub const fn with_block_index(
44        blocks: Vec<CompressedSignedBeaconBlock>,
45        era_state: CompressedBeaconState,
46        slot_index: SlotIndex,
47        state_slot_index: SlotIndex,
48    ) -> Self {
49        Self {
50            blocks,
51            era_state,
52            other_entries: Vec::new(),
53            slot_index: Some(slot_index),
54            state_slot_index,
55        }
56    }
57
58    /// Check if this is a genesis era - no blocks yet
59    pub const fn is_genesis(&self) -> bool {
60        self.blocks.is_empty() && self.slot_index.is_none()
61    }
62
63    /// Add another entry to this group
64    pub fn add_entry(&mut self, entry: Entry) {
65        self.other_entries.push(entry);
66    }
67}
68
69/// [`SlotIndex`] records store offsets to data at specific slots
70/// from the beginning of the index record to the beginning of the corresponding data.
71///
72/// Format: `starting-slot | index | index | index ... | count`
73///
74/// See also <https://github.com/status-im/nimbus-eth2/blob/stable/docs/e2store.md#slotindex>.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct SlotIndex {
77    /// Starting slot number
78    pub starting_slot: u64,
79
80    /// Offsets to data at each slot
81    /// 0 indicates no data for that slot
82    pub offsets: Vec<i64>,
83}
84
85impl SlotIndex {
86    /// Create a new slot index
87    pub const fn new(starting_slot: u64, offsets: Vec<i64>) -> Self {
88        Self { starting_slot, offsets }
89    }
90
91    /// Get the number of slots covered by this index
92    pub const fn slot_count(&self) -> usize {
93        self.offsets.len()
94    }
95
96    /// Get the offset for a specific slot
97    pub fn get_offset(&self, slot_index: usize) -> Option<i64> {
98        self.offsets.get(slot_index).copied()
99    }
100
101    /// Check if a slot has data - non-zero offset
102    pub fn has_data_at_slot(&self, slot_index: usize) -> bool {
103        self.get_offset(slot_index).is_some_and(|offset| offset != 0)
104    }
105}
106
107impl IndexEntry for SlotIndex {
108    fn new(starting_number: u64, offsets: Vec<i64>) -> Self {
109        Self { starting_slot: starting_number, offsets }
110    }
111
112    fn entry_type() -> [u8; 2] {
113        SLOT_INDEX
114    }
115
116    fn starting_number(&self) -> u64 {
117        self.starting_slot
118    }
119
120    fn offsets(&self) -> &[i64] {
121        &self.offsets
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::{
129        e2s_types::{Entry, IndexEntry},
130        test_utils::{create_beacon_block, create_beacon_state},
131    };
132
133    #[test]
134    fn test_slot_index_roundtrip() {
135        let starting_slot = 1000;
136        let offsets = vec![100, 200, 300, 400, 500];
137
138        let slot_index = SlotIndex::new(starting_slot, offsets.clone());
139
140        let entry = slot_index.to_entry();
141
142        // Validate entry type
143        assert_eq!(entry.entry_type, SLOT_INDEX);
144
145        // Convert back to slot index
146        let recovered = SlotIndex::from_entry(&entry).unwrap();
147
148        // Verify fields match
149        assert_eq!(recovered.starting_slot, starting_slot);
150        assert_eq!(recovered.offsets, offsets);
151    }
152    #[test]
153    fn test_slot_index_basic_operations() {
154        let starting_slot = 2000;
155        let offsets = vec![100, 200, 300];
156
157        let slot_index = SlotIndex::new(starting_slot, offsets);
158
159        assert_eq!(slot_index.slot_count(), 3);
160        assert_eq!(slot_index.starting_slot, 2000);
161    }
162
163    #[test]
164    fn test_slot_index_empty_slots() {
165        let starting_slot = 1000;
166        let offsets = vec![100, 0, 300, 0, 500];
167
168        let slot_index = SlotIndex::new(starting_slot, offsets);
169
170        // Test that empty slots return false for has_data_at_slot
171        // slot 1000: offset 100
172        assert!(slot_index.has_data_at_slot(0));
173        // slot 1001: offset 0 - empty
174        assert!(!slot_index.has_data_at_slot(1));
175        // slot 1002: offset 300
176        assert!(slot_index.has_data_at_slot(2));
177        // slot 1003: offset 0 - empty
178        assert!(!slot_index.has_data_at_slot(3));
179        // slot 1004: offset 500
180        assert!(slot_index.has_data_at_slot(4));
181    }
182
183    #[test]
184    fn test_era_group_basic_construction() {
185        let blocks =
186            vec![create_beacon_block(10), create_beacon_block(15), create_beacon_block(20)];
187        let era_state = create_beacon_state(50);
188        let state_slot_index = SlotIndex::new(1000, vec![100, 200, 300]);
189
190        let era_group = EraGroup::new(blocks, era_state, state_slot_index);
191
192        // Verify initial state
193        assert_eq!(era_group.blocks.len(), 3);
194        assert_eq!(era_group.other_entries.len(), 0);
195        assert_eq!(era_group.slot_index, None);
196        assert_eq!(era_group.state_slot_index.starting_slot, 1000);
197        assert_eq!(era_group.state_slot_index.offsets, vec![100, 200, 300]);
198    }
199
200    #[test]
201    fn test_era_group_with_block_index() {
202        let blocks = vec![create_beacon_block(10), create_beacon_block(15)];
203        let era_state = create_beacon_state(50);
204        let block_slot_index = SlotIndex::new(500, vec![50, 100]);
205        let state_slot_index = SlotIndex::new(1000, vec![200, 300]);
206
207        let era_group =
208            EraGroup::with_block_index(blocks, era_state, block_slot_index, state_slot_index);
209
210        // Verify state with block index
211        assert_eq!(era_group.blocks.len(), 2);
212        assert_eq!(era_group.other_entries.len(), 0);
213        assert!(era_group.slot_index.is_some());
214
215        let block_index = era_group.slot_index.as_ref().unwrap();
216        assert_eq!(block_index.starting_slot, 500);
217        assert_eq!(block_index.offsets, vec![50, 100]);
218
219        assert_eq!(era_group.state_slot_index.starting_slot, 1000);
220        assert_eq!(era_group.state_slot_index.offsets, vec![200, 300]);
221    }
222
223    #[test]
224    fn test_era_group_genesis_check() {
225        // Genesis era - no blocks, no block slot index
226        let era_state = create_beacon_state(50);
227        let state_slot_index = SlotIndex::new(0, vec![100]);
228
229        let genesis_era = EraGroup::new(vec![], era_state, state_slot_index);
230        assert!(genesis_era.is_genesis());
231
232        // Non-genesis era - has blocks
233        let blocks = vec![create_beacon_block(10)];
234        let era_state = create_beacon_state(50);
235        let state_slot_index = SlotIndex::new(1000, vec![100]);
236
237        let normal_era = EraGroup::new(blocks, era_state, state_slot_index);
238        assert!(!normal_era.is_genesis());
239
240        // Non-genesis era - has block slot index
241        let era_state = create_beacon_state(50);
242        let block_slot_index = SlotIndex::new(500, vec![50]);
243        let state_slot_index = SlotIndex::new(1000, vec![100]);
244
245        let era_with_index =
246            EraGroup::with_block_index(vec![], era_state, block_slot_index, state_slot_index);
247        assert!(!era_with_index.is_genesis());
248    }
249
250    #[test]
251    fn test_era_group_add_entries() {
252        let blocks = vec![create_beacon_block(10)];
253        let era_state = create_beacon_state(50);
254        let state_slot_index = SlotIndex::new(1000, vec![100]);
255
256        // Create and verify group
257        let mut era_group = EraGroup::new(blocks, era_state, state_slot_index);
258        assert_eq!(era_group.other_entries.len(), 0);
259
260        // Create custom entries with different types
261        let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
262        let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
263
264        // Add those entries
265        era_group.add_entry(entry1);
266        era_group.add_entry(entry2);
267
268        // Verify entries were added correctly
269        assert_eq!(era_group.other_entries.len(), 2);
270        assert_eq!(era_group.other_entries[0].entry_type, [0x01, 0x01]);
271        assert_eq!(era_group.other_entries[0].data, vec![1, 2, 3, 4]);
272        assert_eq!(era_group.other_entries[1].entry_type, [0x02, 0x02]);
273        assert_eq!(era_group.other_entries[1].data, vec![5, 6, 7, 8]);
274    }
275
276    #[test]
277    fn test_index_with_negative_offset() {
278        let mut data = Vec::new();
279        data.extend_from_slice(&0u64.to_le_bytes());
280        data.extend_from_slice(&(-1024i64).to_le_bytes());
281        data.extend_from_slice(&0i64.to_le_bytes());
282        data.extend_from_slice(&2i64.to_le_bytes());
283
284        let entry = Entry::new(SLOT_INDEX, data);
285        let index = SlotIndex::from_entry(&entry).unwrap();
286        let parsed_offset = index.offsets[0];
287        assert_eq!(parsed_offset, -1024);
288    }
289}