1use crate::{
6 common::file_ops::{EraFileId, EraFileType},
7 e2s::{error::E2sError, types::Entry},
8 ere::types::execution::{Accumulator, BlockTuple, MAX_BLOCKS_PER_ERE},
9};
10use alloy_primitives::BlockNumber;
11
12pub const DYNAMIC_BLOCK_INDEX: [u8; 2] = [0x67, 0x32];
14
15pub const MIN_COMPONENTS_PER_BLOCK: u64 = 2;
17
18pub const MAX_COMPONENTS_PER_BLOCK: u64 = 5;
21
22#[derive(Debug)]
30pub struct EreGroup {
31 pub blocks: Vec<BlockTuple>,
33
34 pub other_entries: Vec<Entry>,
36
37 pub accumulator: Option<Accumulator>,
42
43 pub index: DynamicBlockIndex,
45}
46
47impl EreGroup {
48 pub const fn new(
50 blocks: Vec<BlockTuple>,
51 accumulator: Option<Accumulator>,
52 index: DynamicBlockIndex,
53 ) -> Self {
54 Self { blocks, accumulator, index, other_entries: Vec::new() }
55 }
56
57 pub fn add_entry(&mut self, entry: Entry) {
59 self.other_entries.push(entry);
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct DynamicBlockIndex {
79 starting_number: BlockNumber,
81
82 component_count: u64,
84
85 offsets: Vec<i64>,
88}
89
90impl DynamicBlockIndex {
91 pub const fn new(
96 starting_number: BlockNumber,
97 component_count: u64,
98 offsets: Vec<i64>,
99 ) -> Self {
100 Self { starting_number, component_count, offsets }
101 }
102
103 pub const fn starting_number(&self) -> u64 {
105 self.starting_number
106 }
107
108 pub const fn component_count(&self) -> u64 {
110 self.component_count
111 }
112
113 pub const fn block_count(&self) -> usize {
115 if self.component_count == 0 {
116 return 0;
117 }
118 self.offsets.len() / self.component_count as usize
119 }
120
121 pub fn offsets(&self) -> &[i64] {
123 &self.offsets
124 }
125
126 pub fn offsets_for_block(&self, block_number: BlockNumber) -> Option<&[i64]> {
132 if block_number < self.starting_number || self.component_count == 0 {
133 return None;
134 }
135 let index = (block_number - self.starting_number) as usize;
136 let cc = self.component_count as usize;
137 let start = index.checked_mul(cc)?;
138 let end = start.checked_add(cc)?;
139 self.offsets.get(start..end)
140 }
141
142 pub fn to_entry(&self) -> Entry {
146 let block_count = self.block_count();
147 let mut data = Vec::with_capacity(8 + self.offsets.len() * 8 + 16);
148
149 data.extend_from_slice(&self.starting_number.to_le_bytes());
150 data.extend(self.offsets.iter().flat_map(|offset| offset.to_le_bytes()));
151 data.extend_from_slice(&self.component_count.to_le_bytes());
152 data.extend_from_slice(&(block_count as u64).to_le_bytes());
153
154 Entry::new(DYNAMIC_BLOCK_INDEX, data)
155 }
156
157 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
159 entry.ensure_type(DYNAMIC_BLOCK_INDEX, "DynamicBlockIndex")?;
160
161 if entry.data.len() < 24 {
163 return Err(E2sError::Ssz(
164 "DynamicBlockIndex too short: need at least 24 bytes for starting-number, \
165 component-count and count"
166 .to_string(),
167 ));
168 }
169
170 let data = &entry.data;
171 let len = data.len();
172
173 let count = u64::from_le_bytes(
175 data[len - 8..]
176 .try_into()
177 .map_err(|_| E2sError::Ssz("Failed to read count bytes".to_string()))?,
178 ) as usize;
179
180 let component_count = u64::from_le_bytes(
181 data[len - 16..len - 8]
182 .try_into()
183 .map_err(|_| E2sError::Ssz("Failed to read component-count bytes".to_string()))?,
184 );
185
186 if !(MIN_COMPONENTS_PER_BLOCK..=MAX_COMPONENTS_PER_BLOCK).contains(&component_count) {
187 return Err(E2sError::Ssz(format!(
188 "Invalid component-count for DynamicBlockIndex: expected 2-5, got {component_count}"
189 )));
190 }
191
192 let offsets_bytes = len - 24; if !offsets_bytes.is_multiple_of(8) {
196 return Err(E2sError::Ssz(
197 "DynamicBlockIndex offset section is not 8-byte aligned".to_string(),
198 ));
199 }
200 let total_offsets = offsets_bytes / 8;
201
202 if count.checked_mul(component_count as usize) != Some(total_offsets) {
204 return Err(E2sError::Ssz(format!(
205 "DynamicBlockIndex length mismatch: count {count} * component-count \
206 {component_count} does not equal the {total_offsets} stored offsets"
207 )));
208 }
209
210 let starting_number = u64::from_le_bytes(
211 data[0..8]
212 .try_into()
213 .map_err(|_| E2sError::Ssz("Failed to read starting_number bytes".to_string()))?,
214 );
215
216 let mut offsets = Vec::with_capacity(total_offsets);
217 for chunk in data[8..8 + offsets_bytes].as_chunks::<8>().0 {
218 let offset = i64::from_le_bytes(*chunk);
219 offsets.push(offset);
220 }
221
222 Ok(Self { starting_number, component_count, offsets })
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct EreId {
229 pub network_name: String,
231
232 pub start_block: BlockNumber,
234
235 pub block_count: u32,
237
238 pub hash: Option<[u8; 4]>,
241
242 pub include_era_count: bool,
245
246 pub profiles: Vec<EreProfile>,
251}
252
253impl EreId {
254 pub fn new(
256 network_name: impl Into<String>,
257 start_block: BlockNumber,
258 block_count: u32,
259 ) -> Self {
260 Self {
261 network_name: network_name.into(),
262 start_block,
263 block_count,
264 hash: None,
265 include_era_count: false,
266 profiles: Vec::new(),
267 }
268 }
269
270 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
272 self.hash = Some(hash);
273 self
274 }
275
276 pub const fn with_era_count(mut self) -> Self {
278 self.include_era_count = true;
279 self
280 }
281
282 pub fn with_profile(mut self, profile: EreProfile) -> Self {
284 self.profiles.push(profile);
285 self.normalize_profiles();
286 self
287 }
288
289 pub fn with_profiles(mut self, profiles: impl IntoIterator<Item = EreProfile>) -> Self {
291 self.profiles.extend(profiles);
292 self.normalize_profiles();
293 self
294 }
295
296 fn normalize_profiles(&mut self) {
298 self.profiles.sort_unstable();
299 self.profiles.dedup();
300 }
301}
302
303impl EraFileId for EreId {
304 const FILE_TYPE: EraFileType = EraFileType::Ere;
305
306 const ITEMS_PER_ERA: u64 = MAX_BLOCKS_PER_ERE as u64;
307
308 fn network_name(&self) -> &str {
309 &self.network_name
310 }
311
312 fn start_number(&self) -> u64 {
313 self.start_block
314 }
315
316 fn count(&self) -> u32 {
317 self.block_count
318 }
319
320 fn hash(&self) -> Option<[u8; 4]> {
321 self.hash
322 }
323
324 fn include_era_count(&self) -> bool {
325 self.include_era_count
326 }
327
328 fn to_file_name(&self) -> String {
336 let base = Self::FILE_TYPE.format_filename(
337 self.network_name(),
338 self.era_number(),
339 self.hash(),
340 self.include_era_count(),
341 self.era_count(),
342 );
343
344 if self.profiles.is_empty() {
345 return base;
346 }
347
348 let extension = Self::FILE_TYPE.extension();
351 let stem = base.strip_suffix(extension).unwrap_or(base.as_str());
352 let mut name = String::with_capacity(base.len() + self.profiles.len() * 12);
353 name.push_str(stem);
354 for profile in &self.profiles {
355 name.push('-');
356 name.push_str(profile.as_str());
357 }
358 name.push_str(extension);
359 name
360 }
361}
362
363#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
369pub enum EreProfile {
370 NoProofs,
372 NoReceipts,
374}
375
376impl EreProfile {
377 pub const fn as_str(self) -> &'static str {
379 match self {
380 Self::NoProofs => "noproofs",
381 Self::NoReceipts => "noreceipts",
382 }
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::ere::types::execution::{
390 CompressedBody, CompressedHeader, CompressedSlimReceipts, TotalDifficulty,
391 };
392 use alloy_primitives::{B256, U256};
393
394 fn sample_block(data_size: usize) -> BlockTuple {
397 BlockTuple::new(
398 CompressedHeader::new(vec![0xAA; data_size]),
399 CompressedBody::new(vec![0xBB; data_size * 2]),
400 )
401 .with_receipts(CompressedSlimReceipts::new(vec![0xCC; data_size]))
402 .with_total_difficulty(TotalDifficulty::new(U256::from(data_size)))
403 }
404
405 #[test]
406 fn test_dynamic_block_index_roundtrip() {
407 let starting_number = 1000;
408 let component_count = 4;
409 let offsets = vec![100, 200, 300, 400, 500, 600, 700, 800];
411
412 let block_index = DynamicBlockIndex::new(starting_number, component_count, offsets.clone());
413
414 let entry = block_index.to_entry();
415 assert_eq!(entry.entry_type, DYNAMIC_BLOCK_INDEX);
416
417 let recovered = DynamicBlockIndex::from_entry(&entry).unwrap();
418 assert_eq!(recovered, block_index);
419 assert_eq!(recovered.starting_number(), starting_number);
420 assert_eq!(recovered.component_count(), component_count);
421 assert_eq!(recovered.offsets(), offsets);
422 assert_eq!(recovered.block_count(), 2);
423 }
424
425 #[test]
426 fn test_dynamic_block_index_negative_offsets_roundtrip() {
427 for (component_count, offsets) in [
430 (2u64, vec![-2048, -1024, -512, -256]),
431 (5u64, vec![-50, -40, -30, -20, -10, -9, -8, -7, -6, -5]),
432 ] {
433 let index = DynamicBlockIndex::new(1000, component_count, offsets);
434 let recovered = DynamicBlockIndex::from_entry(&index.to_entry()).unwrap();
435 assert_eq!(recovered, index);
436 }
437 }
438
439 #[test]
440 fn test_dynamic_block_index_offset_lookup() {
441 let starting_number = 1000;
442 let component_count = 3;
443 let offsets = vec![10, 20, 30, 40, 50, 60, 70, 80, 90];
445
446 let block_index = DynamicBlockIndex::new(starting_number, component_count, offsets);
447
448 assert_eq!(block_index.offsets_for_block(1000), Some(&[10, 20, 30][..]));
450
451 assert_eq!(block_index.offsets_for_block(1002), Some(&[70, 80, 90][..]));
453
454 assert_eq!(block_index.offsets_for_block(999), None);
456 assert_eq!(block_index.offsets_for_block(1003), None);
457 }
458
459 #[test]
460 fn test_dynamic_block_index_rejects_bad_component_count() {
461 let mut data = Vec::new();
463 data.extend_from_slice(&1000u64.to_le_bytes()); data.extend_from_slice(&42i64.to_le_bytes()); data.extend_from_slice(&1u64.to_le_bytes()); data.extend_from_slice(&1u64.to_le_bytes()); let entry = Entry::new(DYNAMIC_BLOCK_INDEX, data);
468
469 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
470 }
471
472 #[test]
473 fn test_dynamic_block_index_rejects_overflowing_count() {
474 let mut data = Vec::new();
477 data.extend_from_slice(&1000u64.to_le_bytes()); data.extend_from_slice(&2u64.to_le_bytes()); data.extend_from_slice(&(1u64 << 60).to_le_bytes()); let entry = Entry::new(DYNAMIC_BLOCK_INDEX, data);
481
482 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
483 }
484
485 #[test]
486 fn test_dynamic_block_index_rejects_wrong_length() {
487 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200, 300, 400]);
489 let mut entry = block_index.to_entry();
490 entry.data.pop();
491
492 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
493 }
494
495 #[test]
496 fn test_dynamic_block_index_rejects_wrong_type() {
497 let entry = Entry::new([0x66, 0x32], vec![0u8; 24]);
498 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
499 }
500
501 #[test]
502 fn test_ere_group_basic_construction() {
503 let blocks = vec![sample_block(10), sample_block(15), sample_block(20)];
504
505 let accumulator = Accumulator::new(B256::from([0xDD; 32]));
506 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200, 300, 400, 500, 600]);
507
508 let group = EreGroup::new(blocks, Some(accumulator.clone()), block_index);
509
510 assert_eq!(group.blocks.len(), 3);
511 assert_eq!(group.other_entries.len(), 0);
512 assert_eq!(group.accumulator.unwrap().root, accumulator.root);
513 assert_eq!(group.index.starting_number(), 1000);
514 assert_eq!(group.index.offsets(), vec![100, 200, 300, 400, 500, 600]);
515 }
516
517 #[test]
518 fn test_ere_group_without_accumulator() {
519 let blocks = vec![sample_block(10)];
521 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200]);
522
523 let group = EreGroup::new(blocks, None, block_index);
524
525 assert!(group.accumulator.is_none());
526 assert_eq!(group.blocks.len(), 1);
527 }
528
529 #[test]
530 fn test_ere_group_add_entries() {
531 let blocks = vec![sample_block(10)];
532 let accumulator = Accumulator::new(B256::from([0xDD; 32]));
533 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200]);
534
535 let mut group = EreGroup::new(blocks, Some(accumulator), block_index);
536 assert_eq!(group.other_entries.len(), 0);
537
538 group.add_entry(Entry::new([0x01, 0x01], vec![1, 2, 3, 4]));
539 group.add_entry(Entry::new([0x02, 0x02], vec![5, 6, 7, 8]));
540
541 assert_eq!(group.other_entries.len(), 2);
542 assert_eq!(group.other_entries[0].entry_type, [0x01, 0x01]);
543 assert_eq!(group.other_entries[1].data, vec![5, 6, 7, 8]);
544 }
545
546 #[test]
547 fn test_ere_group_with_mismatched_index() {
548 let blocks = vec![sample_block(10), sample_block(15)];
550 let index = DynamicBlockIndex::new(2000, 2, vec![100, 200, 300, 400, 500, 600]); let group = EreGroup::new(blocks, None, index);
552 assert_eq!(group.blocks.len(), 2);
553 assert_eq!(group.index.starting_number(), 2000);
554 }
555
556 #[test_case::test_case(
557 EreId::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]),
558 "mainnet-00000-5ec1ffb8.ere";
559 "Mainnet era 0"
560 )]
561 #[test_case::test_case(
562 EreId::new("mainnet", 8192, 8192).with_hash([0x5e, 0xcb, 0x9b, 0xf9]),
563 "mainnet-00001-5ecb9bf9.ere";
564 "Mainnet era 1"
565 )]
566 #[test_case::test_case(
567 EreId::new("sepolia", 0, 8192).with_hash([0x90, 0x91, 0x84, 0x72]),
568 "sepolia-00000-90918472.ere";
569 "Sepolia era 0"
570 )]
571 #[test_case::test_case(
572 EreId::new("mainnet", 1000, 100),
573 "mainnet-00000-00000000.ere";
574 "ID without hash"
575 )]
576 fn test_ere_id_file_naming(id: EreId, expected_file_name: &str) {
577 assert_eq!(id.to_file_name(), expected_file_name);
578 }
579
580 #[test_case::test_case(
582 EreId::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]).with_era_count(),
583 "mainnet-00000-00001-5ec1ffb8.ere";
584 "Mainnet era 0 with count"
585 )]
586 #[test_case::test_case(
587 EreId::new("mainnet", 8000, 500).with_hash([0xab, 0xcd, 0xef, 0x12]).with_era_count(),
588 "mainnet-00000-00002-abcdef12.ere";
589 "Spanning two eras with count"
590 )]
591 fn test_ere_id_file_naming_with_era_count(id: EreId, expected_file_name: &str) {
592 assert_eq!(id.to_file_name(), expected_file_name);
593 }
594
595 #[test_case::test_case(
597 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]),
598 "mainnet-00000-4bb7de2e.ere";
599 "Default profile, no postfix"
600 )]
601 #[test_case::test_case(
602 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profile(EreProfile::NoProofs),
603 "mainnet-00000-4bb7de2e-noproofs.ere";
604 "noproofs profile"
605 )]
606 #[test_case::test_case(
607 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profile(EreProfile::NoReceipts),
608 "mainnet-00000-4bb7de2e-noreceipts.ere";
609 "noreceipts profile"
610 )]
611 #[test_case::test_case(
612 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profiles([EreProfile::NoProofs, EreProfile::NoReceipts]),
613 "mainnet-00000-4bb7de2e-noproofs-noreceipts.ere";
614 "Combined profiles"
615 )]
616 #[test_case::test_case(
617 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profile(EreProfile::NoReceipts).with_profile(EreProfile::NoProofs).with_profile(EreProfile::NoReceipts),
619 "mainnet-00000-4bb7de2e-noproofs-noreceipts.ere";
620 "Profiles normalized to alphabetical order"
621 )]
622 fn test_ere_id_file_naming_with_profiles(id: EreId, expected_file_name: &str) {
623 assert_eq!(id.to_file_name(), expected_file_name);
624 }
625}