1use crate::{
6 common::file_ops::{EraFileId, EraFileType},
7 e2s::types::{Entry, IndexEntry, SLOT_INDEX},
8 era::types::consensus::{CompressedBeaconState, CompressedSignedBeaconBlock},
9};
10
11pub const SLOTS_PER_HISTORICAL_ROOT: u64 = crate::common::MAX_ENTRIES_PER_ERA;
15
16#[derive(Debug)]
21pub struct EraGroup {
22 pub blocks: Vec<CompressedSignedBeaconBlock>,
24
25 pub era_state: CompressedBeaconState,
27
28 pub other_entries: Vec<Entry>,
30
31 pub slot_index: Option<SlotIndex>,
33
34 pub state_slot_index: SlotIndex,
36}
37
38impl EraGroup {
39 pub const fn new(
41 blocks: Vec<CompressedSignedBeaconBlock>,
42 era_state: CompressedBeaconState,
43 state_slot_index: SlotIndex,
44 ) -> Self {
45 Self { blocks, era_state, other_entries: Vec::new(), slot_index: None, state_slot_index }
46 }
47
48 pub const fn with_block_index(
50 blocks: Vec<CompressedSignedBeaconBlock>,
51 era_state: CompressedBeaconState,
52 slot_index: SlotIndex,
53 state_slot_index: SlotIndex,
54 ) -> Self {
55 Self {
56 blocks,
57 era_state,
58 other_entries: Vec::new(),
59 slot_index: Some(slot_index),
60 state_slot_index,
61 }
62 }
63
64 pub const fn is_genesis(&self) -> bool {
66 self.blocks.is_empty() && self.slot_index.is_none()
67 }
68
69 pub fn add_entry(&mut self, entry: Entry) {
71 self.other_entries.push(entry);
72 }
73
74 pub const fn slot_range(&self) -> (u64, u32) {
76 if let Some(ref block_index) = self.slot_index {
77 (block_index.starting_slot, block_index.slot_count() as u32)
79 } else {
80 (self.state_slot_index.starting_slot, 0)
83 }
84 }
85
86 pub const fn starting_slot(&self) -> u64 {
88 self.slot_range().0
89 }
90
91 pub const fn slot_count(&self) -> u32 {
93 self.slot_range().1
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct SlotIndex {
105 pub starting_slot: u64,
107
108 pub offsets: Vec<i64>,
111}
112
113impl SlotIndex {
114 pub const fn new(starting_slot: u64, offsets: Vec<i64>) -> Self {
116 Self { starting_slot, offsets }
117 }
118
119 pub const fn slot_count(&self) -> usize {
121 self.offsets.len()
122 }
123
124 pub fn get_offset(&self, slot_index: usize) -> Option<i64> {
126 self.offsets.get(slot_index).copied()
127 }
128
129 pub fn has_data_at_slot(&self, slot_index: usize) -> bool {
131 self.get_offset(slot_index).is_some_and(|offset| offset != 0)
132 }
133}
134
135impl IndexEntry for SlotIndex {
136 fn new(starting_number: u64, offsets: Vec<i64>) -> Self {
137 Self { starting_slot: starting_number, offsets }
138 }
139
140 fn entry_type() -> [u8; 2] {
141 SLOT_INDEX
142 }
143
144 fn starting_number(&self) -> u64 {
145 self.starting_slot
146 }
147
148 fn offsets(&self) -> &[i64] {
149 &self.offsets
150 }
151}
152
153#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct EraId {
156 pub network_name: String,
158
159 pub start_slot: u64,
161
162 pub slot_count: u32,
164
165 pub hash: Option<[u8; 4]>,
168
169 include_era_count: bool,
172}
173
174impl EraId {
175 pub fn new(network_name: impl Into<String>, start_slot: u64, slot_count: u32) -> Self {
177 Self {
178 network_name: network_name.into(),
179 start_slot,
180 slot_count,
181 hash: None,
182 include_era_count: false,
183 }
184 }
185
186 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
188 self.hash = Some(hash);
189 self
190 }
191
192 pub const fn with_era_count(mut self) -> Self {
194 self.include_era_count = true;
195 self
196 }
197}
198
199impl EraFileId for EraId {
200 const FILE_TYPE: EraFileType = EraFileType::Era;
201
202 const ITEMS_PER_ERA: u64 = SLOTS_PER_HISTORICAL_ROOT;
203
204 fn network_name(&self) -> &str {
205 &self.network_name
206 }
207
208 fn start_number(&self) -> u64 {
209 self.start_slot
210 }
211
212 fn count(&self) -> u32 {
213 self.slot_count
214 }
215
216 fn hash(&self) -> Option<[u8; 4]> {
217 self.hash
218 }
219
220 fn include_era_count(&self) -> bool {
221 self.include_era_count
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::{
229 e2s::types::{Entry, IndexEntry},
230 test_utils::{create_beacon_block, create_beacon_state},
231 };
232
233 #[test]
234 fn test_slot_index_roundtrip() {
235 let starting_slot = 1000;
236 let offsets = vec![100, 200, 300, 400, 500];
237
238 let slot_index = SlotIndex::new(starting_slot, offsets.clone());
239
240 let entry = slot_index.to_entry();
241
242 assert_eq!(entry.entry_type, SLOT_INDEX);
244
245 let recovered = SlotIndex::from_entry(&entry).unwrap();
247
248 assert_eq!(recovered.starting_slot, starting_slot);
250 assert_eq!(recovered.offsets, offsets);
251 }
252 #[test]
253 fn test_slot_index_basic_operations() {
254 let starting_slot = 2000;
255 let offsets = vec![100, 200, 300];
256
257 let slot_index = SlotIndex::new(starting_slot, offsets);
258
259 assert_eq!(slot_index.slot_count(), 3);
260 assert_eq!(slot_index.starting_slot, 2000);
261 }
262
263 #[test]
264 fn test_slot_index_empty_slots() {
265 let starting_slot = 1000;
266 let offsets = vec![100, 0, 300, 0, 500];
267
268 let slot_index = SlotIndex::new(starting_slot, offsets);
269
270 assert!(slot_index.has_data_at_slot(0));
273 assert!(!slot_index.has_data_at_slot(1));
275 assert!(slot_index.has_data_at_slot(2));
277 assert!(!slot_index.has_data_at_slot(3));
279 assert!(slot_index.has_data_at_slot(4));
281 }
282
283 #[test]
284 fn test_era_group_basic_construction() {
285 let blocks =
286 vec![create_beacon_block(10), create_beacon_block(15), create_beacon_block(20)];
287 let era_state = create_beacon_state(50);
288 let state_slot_index = SlotIndex::new(1000, vec![100, 200, 300]);
289
290 let era_group = EraGroup::new(blocks, era_state, state_slot_index);
291
292 assert_eq!(era_group.blocks.len(), 3);
294 assert_eq!(era_group.other_entries.len(), 0);
295 assert_eq!(era_group.slot_index, None);
296 assert_eq!(era_group.state_slot_index.starting_slot, 1000);
297 assert_eq!(era_group.state_slot_index.offsets, vec![100, 200, 300]);
298 }
299
300 #[test]
301 fn test_era_group_with_block_index() {
302 let blocks = vec![create_beacon_block(10), create_beacon_block(15)];
303 let era_state = create_beacon_state(50);
304 let block_slot_index = SlotIndex::new(500, vec![50, 100]);
305 let state_slot_index = SlotIndex::new(1000, vec![200, 300]);
306
307 let era_group =
308 EraGroup::with_block_index(blocks, era_state, block_slot_index, state_slot_index);
309
310 assert_eq!(era_group.blocks.len(), 2);
312 assert_eq!(era_group.other_entries.len(), 0);
313 assert!(era_group.slot_index.is_some());
314
315 let block_index = era_group.slot_index.as_ref().unwrap();
316 assert_eq!(block_index.starting_slot, 500);
317 assert_eq!(block_index.offsets, vec![50, 100]);
318
319 assert_eq!(era_group.state_slot_index.starting_slot, 1000);
320 assert_eq!(era_group.state_slot_index.offsets, vec![200, 300]);
321 }
322
323 #[test]
324 fn test_era_group_genesis_check() {
325 let era_state = create_beacon_state(50);
327 let state_slot_index = SlotIndex::new(0, vec![100]);
328
329 let genesis_era = EraGroup::new(vec![], era_state, state_slot_index);
330 assert!(genesis_era.is_genesis());
331
332 let blocks = vec![create_beacon_block(10)];
334 let era_state = create_beacon_state(50);
335 let state_slot_index = SlotIndex::new(1000, vec![100]);
336
337 let normal_era = EraGroup::new(blocks, era_state, state_slot_index);
338 assert!(!normal_era.is_genesis());
339
340 let era_state = create_beacon_state(50);
342 let block_slot_index = SlotIndex::new(500, vec![50]);
343 let state_slot_index = SlotIndex::new(1000, vec![100]);
344
345 let era_with_index =
346 EraGroup::with_block_index(vec![], era_state, block_slot_index, state_slot_index);
347 assert!(!era_with_index.is_genesis());
348 }
349
350 #[test]
351 fn test_era_group_add_entries() {
352 let blocks = vec![create_beacon_block(10)];
353 let era_state = create_beacon_state(50);
354 let state_slot_index = SlotIndex::new(1000, vec![100]);
355
356 let mut era_group = EraGroup::new(blocks, era_state, state_slot_index);
358 assert_eq!(era_group.other_entries.len(), 0);
359
360 let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
362 let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
363
364 era_group.add_entry(entry1);
366 era_group.add_entry(entry2);
367
368 assert_eq!(era_group.other_entries.len(), 2);
370 assert_eq!(era_group.other_entries[0].entry_type, [0x01, 0x01]);
371 assert_eq!(era_group.other_entries[0].data, vec![1, 2, 3, 4]);
372 assert_eq!(era_group.other_entries[1].entry_type, [0x02, 0x02]);
373 assert_eq!(era_group.other_entries[1].data, vec![5, 6, 7, 8]);
374 }
375
376 #[test]
377 fn test_index_with_negative_offset() {
378 let mut data = Vec::new();
379 data.extend_from_slice(&0u64.to_le_bytes());
380 data.extend_from_slice(&(-1024i64).to_le_bytes());
381 data.extend_from_slice(&0i64.to_le_bytes());
382 data.extend_from_slice(&2i64.to_le_bytes());
383
384 let entry = Entry::new(SLOT_INDEX, data);
385 let index = SlotIndex::from_entry(&entry).unwrap();
386 let parsed_offset = index.offsets[0];
387 assert_eq!(parsed_offset, -1024);
388 }
389
390 #[test_case::test_case(
391 EraId::new("mainnet", 0, 8192).with_hash([0x4b, 0x36, 0x3d, 0xb9]),
392 "mainnet-00000-4b363db9.era";
393 "Mainnet era 0"
394 )]
395 #[test_case::test_case(
396 EraId::new("mainnet", 8192, 8192).with_hash([0x40, 0xcf, 0x2f, 0x3c]),
397 "mainnet-00001-40cf2f3c.era";
398 "Mainnet era 1"
399 )]
400 #[test_case::test_case(
401 EraId::new("mainnet", 0, 8192),
402 "mainnet-00000-00000000.era";
403 "Without hash"
404 )]
405 fn test_era_id_file_naming(id: EraId, expected_file_name: &str) {
406 let actual_file_name = id.to_file_name();
407 assert_eq!(actual_file_name, expected_file_name);
408 }
409
410 #[test_case::test_case(
412 EraId::new("mainnet", 0, 8192).with_hash([0x4b, 0x36, 0x3d, 0xb9]).with_era_count(),
413 "mainnet-00000-00001-4b363db9.era";
414 "Mainnet era 0 with count"
415 )]
416 #[test_case::test_case(
417 EraId::new("mainnet", 8000, 500).with_hash([0xab, 0xcd, 0xef, 0x12]).with_era_count(),
418 "mainnet-00000-00002-abcdef12.era";
419 "Spanning two eras with count"
420 )]
421 fn test_era_id_file_naming_with_era_count(id: EraId, expected_file_name: &str) {
422 let actual_file_name = id.to_file_name();
423 assert_eq!(actual_file_name, expected_file_name);
424 }
425}