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 = 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 include_era_count: bool,
170}
171
172impl EraId {
173 pub fn new(network_name: impl Into<String>, start_slot: u64, slot_count: u32) -> Self {
175 Self {
176 network_name: network_name.into(),
177 start_slot,
178 slot_count,
179 hash: None,
180 include_era_count: false,
181 }
182 }
183
184 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
186 self.hash = Some(hash);
187 self
188 }
189
190 pub const fn with_era_count(mut self) -> Self {
192 self.include_era_count = true;
193 self
194 }
195}
196
197impl EraFileId for EraId {
198 const FILE_TYPE: EraFileType = EraFileType::Era;
199
200 const ITEMS_PER_ERA: u64 = SLOTS_PER_HISTORICAL_ROOT;
201
202 fn network_name(&self) -> &str {
203 &self.network_name
204 }
205
206 fn start_number(&self) -> u64 {
207 self.start_slot
208 }
209
210 fn count(&self) -> u32 {
211 self.slot_count
212 }
213
214 fn hash(&self) -> Option<[u8; 4]> {
215 self.hash
216 }
217
218 fn include_era_count(&self) -> bool {
219 self.include_era_count
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use crate::{
227 e2s::types::{Entry, IndexEntry},
228 test_utils::{create_beacon_block, create_beacon_state},
229 };
230
231 #[test]
232 fn test_slot_index_roundtrip() {
233 let starting_slot = 1000;
234 let offsets = vec![100, 200, 300, 400, 500];
235
236 let slot_index = SlotIndex::new(starting_slot, offsets.clone());
237
238 let entry = slot_index.to_entry();
239
240 assert_eq!(entry.entry_type, SLOT_INDEX);
242
243 let recovered = SlotIndex::from_entry(&entry).unwrap();
245
246 assert_eq!(recovered.starting_slot, starting_slot);
248 assert_eq!(recovered.offsets, offsets);
249 }
250 #[test]
251 fn test_slot_index_basic_operations() {
252 let starting_slot = 2000;
253 let offsets = vec![100, 200, 300];
254
255 let slot_index = SlotIndex::new(starting_slot, offsets);
256
257 assert_eq!(slot_index.slot_count(), 3);
258 assert_eq!(slot_index.starting_slot, 2000);
259 }
260
261 #[test]
262 fn test_slot_index_empty_slots() {
263 let starting_slot = 1000;
264 let offsets = vec![100, 0, 300, 0, 500];
265
266 let slot_index = SlotIndex::new(starting_slot, offsets);
267
268 assert!(slot_index.has_data_at_slot(0));
271 assert!(!slot_index.has_data_at_slot(1));
273 assert!(slot_index.has_data_at_slot(2));
275 assert!(!slot_index.has_data_at_slot(3));
277 assert!(slot_index.has_data_at_slot(4));
279 }
280
281 #[test]
282 fn test_era_group_basic_construction() {
283 let blocks =
284 vec![create_beacon_block(10), create_beacon_block(15), create_beacon_block(20)];
285 let era_state = create_beacon_state(50);
286 let state_slot_index = SlotIndex::new(1000, vec![100, 200, 300]);
287
288 let era_group = EraGroup::new(blocks, era_state, state_slot_index);
289
290 assert_eq!(era_group.blocks.len(), 3);
292 assert_eq!(era_group.other_entries.len(), 0);
293 assert_eq!(era_group.slot_index, None);
294 assert_eq!(era_group.state_slot_index.starting_slot, 1000);
295 assert_eq!(era_group.state_slot_index.offsets, vec![100, 200, 300]);
296 }
297
298 #[test]
299 fn test_era_group_with_block_index() {
300 let blocks = vec![create_beacon_block(10), create_beacon_block(15)];
301 let era_state = create_beacon_state(50);
302 let block_slot_index = SlotIndex::new(500, vec![50, 100]);
303 let state_slot_index = SlotIndex::new(1000, vec![200, 300]);
304
305 let era_group =
306 EraGroup::with_block_index(blocks, era_state, block_slot_index, state_slot_index);
307
308 assert_eq!(era_group.blocks.len(), 2);
310 assert_eq!(era_group.other_entries.len(), 0);
311 assert!(era_group.slot_index.is_some());
312
313 let block_index = era_group.slot_index.as_ref().unwrap();
314 assert_eq!(block_index.starting_slot, 500);
315 assert_eq!(block_index.offsets, vec![50, 100]);
316
317 assert_eq!(era_group.state_slot_index.starting_slot, 1000);
318 assert_eq!(era_group.state_slot_index.offsets, vec![200, 300]);
319 }
320
321 #[test]
322 fn test_era_group_genesis_check() {
323 let era_state = create_beacon_state(50);
325 let state_slot_index = SlotIndex::new(0, vec![100]);
326
327 let genesis_era = EraGroup::new(vec![], era_state, state_slot_index);
328 assert!(genesis_era.is_genesis());
329
330 let blocks = vec![create_beacon_block(10)];
332 let era_state = create_beacon_state(50);
333 let state_slot_index = SlotIndex::new(1000, vec![100]);
334
335 let normal_era = EraGroup::new(blocks, era_state, state_slot_index);
336 assert!(!normal_era.is_genesis());
337
338 let era_state = create_beacon_state(50);
340 let block_slot_index = SlotIndex::new(500, vec![50]);
341 let state_slot_index = SlotIndex::new(1000, vec![100]);
342
343 let era_with_index =
344 EraGroup::with_block_index(vec![], era_state, block_slot_index, state_slot_index);
345 assert!(!era_with_index.is_genesis());
346 }
347
348 #[test]
349 fn test_era_group_add_entries() {
350 let blocks = vec![create_beacon_block(10)];
351 let era_state = create_beacon_state(50);
352 let state_slot_index = SlotIndex::new(1000, vec![100]);
353
354 let mut era_group = EraGroup::new(blocks, era_state, state_slot_index);
356 assert_eq!(era_group.other_entries.len(), 0);
357
358 let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
360 let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
361
362 era_group.add_entry(entry1);
364 era_group.add_entry(entry2);
365
366 assert_eq!(era_group.other_entries.len(), 2);
368 assert_eq!(era_group.other_entries[0].entry_type, [0x01, 0x01]);
369 assert_eq!(era_group.other_entries[0].data, vec![1, 2, 3, 4]);
370 assert_eq!(era_group.other_entries[1].entry_type, [0x02, 0x02]);
371 assert_eq!(era_group.other_entries[1].data, vec![5, 6, 7, 8]);
372 }
373
374 #[test]
375 fn test_index_with_negative_offset() {
376 let mut data = Vec::new();
377 data.extend_from_slice(&0u64.to_le_bytes());
378 data.extend_from_slice(&(-1024i64).to_le_bytes());
379 data.extend_from_slice(&0i64.to_le_bytes());
380 data.extend_from_slice(&2i64.to_le_bytes());
381
382 let entry = Entry::new(SLOT_INDEX, data);
383 let index = SlotIndex::from_entry(&entry).unwrap();
384 let parsed_offset = index.offsets[0];
385 assert_eq!(parsed_offset, -1024);
386 }
387
388 #[test_case::test_case(
389 EraId::new("mainnet", 0, 8192).with_hash([0x4b, 0x36, 0x3d, 0xb9]),
390 "mainnet-00000-4b363db9.era";
391 "Mainnet era 0"
392 )]
393 #[test_case::test_case(
394 EraId::new("mainnet", 8192, 8192).with_hash([0x40, 0xcf, 0x2f, 0x3c]),
395 "mainnet-00001-40cf2f3c.era";
396 "Mainnet era 1"
397 )]
398 #[test_case::test_case(
399 EraId::new("mainnet", 0, 8192),
400 "mainnet-00000-00000000.era";
401 "Without hash"
402 )]
403 fn test_era_id_file_naming(id: EraId, expected_file_name: &str) {
404 let actual_file_name = id.to_file_name();
405 assert_eq!(actual_file_name, expected_file_name);
406 }
407
408 #[test_case::test_case(
410 EraId::new("mainnet", 0, 8192).with_hash([0x4b, 0x36, 0x3d, 0xb9]).with_era_count(),
411 "mainnet-00000-00001-4b363db9.era";
412 "Mainnet era 0 with count"
413 )]
414 #[test_case::test_case(
415 EraId::new("mainnet", 8000, 500).with_hash([0xab, 0xcd, 0xef, 0x12]).with_era_count(),
416 "mainnet-00000-00002-abcdef12.era";
417 "Spanning two eras with count"
418 )]
419 fn test_era_id_file_naming_with_era_count(id: EraId, expected_file_name: &str) {
420 let actual_file_name = id.to_file_name();
421 assert_eq!(actual_file_name, expected_file_name);
422 }
423}