1use crate::{
6 common::file_ops::EraFileId,
7 e2s::types::{Entry, IndexEntry, SLOT_INDEX},
8 era::types::consensus::{CompressedBeaconState, CompressedSignedBeaconBlock},
9};
10
11pub const SLOTS_PER_HISTORICAL_ROOT: u64 = 8192;
13
14#[derive(Debug)]
19pub struct EraGroup {
20 pub blocks: Vec<CompressedSignedBeaconBlock>,
22
23 pub era_state: CompressedBeaconState,
25
26 pub other_entries: Vec<Entry>,
28
29 pub slot_index: Option<SlotIndex>,
31
32 pub state_slot_index: SlotIndex,
34}
35
36impl EraGroup {
37 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 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 pub const fn is_genesis(&self) -> bool {
64 self.blocks.is_empty() && self.slot_index.is_none()
65 }
66
67 pub fn add_entry(&mut self, entry: Entry) {
69 self.other_entries.push(entry);
70 }
71
72 pub const fn slot_range(&self) -> (u64, u32) {
74 if let Some(ref block_index) = self.slot_index {
75 (block_index.starting_slot, block_index.slot_count() as u32)
77 } else {
78 (self.state_slot_index.starting_slot, 0)
81 }
82 }
83
84 pub const fn starting_slot(&self) -> u64 {
86 self.slot_range().0
87 }
88
89 pub const fn slot_count(&self) -> u32 {
91 self.slot_range().1
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct SlotIndex {
103 pub starting_slot: u64,
105
106 pub offsets: Vec<i64>,
109}
110
111impl SlotIndex {
112 pub const fn new(starting_slot: u64, offsets: Vec<i64>) -> Self {
114 Self { starting_slot, offsets }
115 }
116
117 pub const fn slot_count(&self) -> usize {
119 self.offsets.len()
120 }
121
122 pub fn get_offset(&self, slot_index: usize) -> Option<i64> {
124 self.offsets.get(slot_index).copied()
125 }
126
127 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#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct EraId {
154 pub network_name: String,
156
157 pub start_slot: u64,
159
160 pub slot_count: u32,
162
163 pub hash: Option<[u8; 4]>,
166}
167
168impl EraId {
169 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 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
176 self.hash = Some(hash);
177 self
178 }
179
180 pub const fn era_number(&self) -> u64 {
182 self.start_slot / SLOTS_PER_HISTORICAL_ROOT
183 }
184
185 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 let last_slot = self.start_slot + self.slot_count as u64 - 1;
198 let last_era = last_slot / SLOTS_PER_HISTORICAL_ROOT;
200 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 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 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 assert_eq!(entry.entry_type, SLOT_INDEX);
257
258 let recovered = SlotIndex::from_entry(&entry).unwrap();
260
261 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 assert!(slot_index.has_data_at_slot(0));
286 assert!(!slot_index.has_data_at_slot(1));
288 assert!(slot_index.has_data_at_slot(2));
290 assert!(!slot_index.has_data_at_slot(3));
292 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 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 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 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 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 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 let mut era_group = EraGroup::new(blocks, era_state, state_slot_index);
371 assert_eq!(era_group.other_entries.len(), 0);
372
373 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 era_group.add_entry(entry1);
379 era_group.add_entry(entry2);
380
381 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}