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].chunks_exact(8) {
218 let offset = i64::from_le_bytes(
219 chunk.try_into().map_err(|_| E2sError::Ssz("Failed to read offset".to_string()))?,
220 );
221 offsets.push(offset);
222 }
223
224 Ok(Self { starting_number, component_count, offsets })
225 }
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct EreId {
231 pub network_name: String,
233
234 pub start_block: BlockNumber,
236
237 pub block_count: u32,
239
240 pub hash: Option<[u8; 4]>,
243
244 pub include_era_count: bool,
247
248 pub profiles: Vec<EreProfile>,
253}
254
255impl EreId {
256 pub fn new(
258 network_name: impl Into<String>,
259 start_block: BlockNumber,
260 block_count: u32,
261 ) -> Self {
262 Self {
263 network_name: network_name.into(),
264 start_block,
265 block_count,
266 hash: None,
267 include_era_count: false,
268 profiles: Vec::new(),
269 }
270 }
271
272 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
274 self.hash = Some(hash);
275 self
276 }
277
278 pub const fn with_era_count(mut self) -> Self {
280 self.include_era_count = true;
281 self
282 }
283
284 pub fn with_profile(mut self, profile: EreProfile) -> Self {
286 self.profiles.push(profile);
287 self.normalize_profiles();
288 self
289 }
290
291 pub fn with_profiles(mut self, profiles: impl IntoIterator<Item = EreProfile>) -> Self {
293 self.profiles.extend(profiles);
294 self.normalize_profiles();
295 self
296 }
297
298 fn normalize_profiles(&mut self) {
300 self.profiles.sort_unstable();
301 self.profiles.dedup();
302 }
303}
304
305impl EraFileId for EreId {
306 const FILE_TYPE: EraFileType = EraFileType::Ere;
307
308 const ITEMS_PER_ERA: u64 = MAX_BLOCKS_PER_ERE as u64;
309
310 fn network_name(&self) -> &str {
311 &self.network_name
312 }
313
314 fn start_number(&self) -> u64 {
315 self.start_block
316 }
317
318 fn count(&self) -> u32 {
319 self.block_count
320 }
321
322 fn hash(&self) -> Option<[u8; 4]> {
323 self.hash
324 }
325
326 fn include_era_count(&self) -> bool {
327 self.include_era_count
328 }
329
330 fn to_file_name(&self) -> String {
338 let base = Self::FILE_TYPE.format_filename(
339 self.network_name(),
340 self.era_number(),
341 self.hash(),
342 self.include_era_count(),
343 self.era_count(),
344 );
345
346 if self.profiles.is_empty() {
347 return base;
348 }
349
350 let extension = Self::FILE_TYPE.extension();
353 let stem = base.strip_suffix(extension).unwrap_or(base.as_str());
354 let mut name = String::with_capacity(base.len() + self.profiles.len() * 12);
355 name.push_str(stem);
356 for profile in &self.profiles {
357 name.push('-');
358 name.push_str(profile.as_str());
359 }
360 name.push_str(extension);
361 name
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
371pub enum EreProfile {
372 NoProofs,
374 NoReceipts,
376}
377
378impl EreProfile {
379 pub const fn as_str(self) -> &'static str {
381 match self {
382 Self::NoProofs => "noproofs",
383 Self::NoReceipts => "noreceipts",
384 }
385 }
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391 use crate::ere::types::execution::{
392 CompressedBody, CompressedHeader, CompressedSlimReceipts, TotalDifficulty,
393 };
394 use alloy_primitives::{B256, U256};
395
396 fn sample_block(data_size: usize) -> BlockTuple {
399 BlockTuple::new(
400 CompressedHeader::new(vec![0xAA; data_size]),
401 CompressedBody::new(vec![0xBB; data_size * 2]),
402 )
403 .with_receipts(CompressedSlimReceipts::new(vec![0xCC; data_size]))
404 .with_total_difficulty(TotalDifficulty::new(U256::from(data_size)))
405 }
406
407 #[test]
408 fn test_dynamic_block_index_roundtrip() {
409 let starting_number = 1000;
410 let component_count = 4;
411 let offsets = vec![100, 200, 300, 400, 500, 600, 700, 800];
413
414 let block_index = DynamicBlockIndex::new(starting_number, component_count, offsets.clone());
415
416 let entry = block_index.to_entry();
417 assert_eq!(entry.entry_type, DYNAMIC_BLOCK_INDEX);
418
419 let recovered = DynamicBlockIndex::from_entry(&entry).unwrap();
420 assert_eq!(recovered, block_index);
421 assert_eq!(recovered.starting_number(), starting_number);
422 assert_eq!(recovered.component_count(), component_count);
423 assert_eq!(recovered.offsets(), offsets);
424 assert_eq!(recovered.block_count(), 2);
425 }
426
427 #[test]
428 fn test_dynamic_block_index_negative_offsets_roundtrip() {
429 for (component_count, offsets) in [
432 (2u64, vec![-2048, -1024, -512, -256]),
433 (5u64, vec![-50, -40, -30, -20, -10, -9, -8, -7, -6, -5]),
434 ] {
435 let index = DynamicBlockIndex::new(1000, component_count, offsets);
436 let recovered = DynamicBlockIndex::from_entry(&index.to_entry()).unwrap();
437 assert_eq!(recovered, index);
438 }
439 }
440
441 #[test]
442 fn test_dynamic_block_index_offset_lookup() {
443 let starting_number = 1000;
444 let component_count = 3;
445 let offsets = vec![10, 20, 30, 40, 50, 60, 70, 80, 90];
447
448 let block_index = DynamicBlockIndex::new(starting_number, component_count, offsets);
449
450 assert_eq!(block_index.offsets_for_block(1000), Some(&[10, 20, 30][..]));
452
453 assert_eq!(block_index.offsets_for_block(1002), Some(&[70, 80, 90][..]));
455
456 assert_eq!(block_index.offsets_for_block(999), None);
458 assert_eq!(block_index.offsets_for_block(1003), None);
459 }
460
461 #[test]
462 fn test_dynamic_block_index_rejects_bad_component_count() {
463 let mut data = Vec::new();
465 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);
470
471 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
472 }
473
474 #[test]
475 fn test_dynamic_block_index_rejects_overflowing_count() {
476 let mut data = Vec::new();
479 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);
483
484 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
485 }
486
487 #[test]
488 fn test_dynamic_block_index_rejects_wrong_length() {
489 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200, 300, 400]);
491 let mut entry = block_index.to_entry();
492 entry.data.pop();
493
494 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
495 }
496
497 #[test]
498 fn test_dynamic_block_index_rejects_wrong_type() {
499 let entry = Entry::new([0x66, 0x32], vec![0u8; 24]);
500 assert!(DynamicBlockIndex::from_entry(&entry).is_err());
501 }
502
503 #[test]
504 fn test_ere_group_basic_construction() {
505 let blocks = vec![sample_block(10), sample_block(15), sample_block(20)];
506
507 let accumulator = Accumulator::new(B256::from([0xDD; 32]));
508 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200, 300, 400, 500, 600]);
509
510 let group = EreGroup::new(blocks, Some(accumulator.clone()), block_index);
511
512 assert_eq!(group.blocks.len(), 3);
513 assert_eq!(group.other_entries.len(), 0);
514 assert_eq!(group.accumulator.unwrap().root, accumulator.root);
515 assert_eq!(group.index.starting_number(), 1000);
516 assert_eq!(group.index.offsets(), vec![100, 200, 300, 400, 500, 600]);
517 }
518
519 #[test]
520 fn test_ere_group_without_accumulator() {
521 let blocks = vec![sample_block(10)];
523 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200]);
524
525 let group = EreGroup::new(blocks, None, block_index);
526
527 assert!(group.accumulator.is_none());
528 assert_eq!(group.blocks.len(), 1);
529 }
530
531 #[test]
532 fn test_ere_group_add_entries() {
533 let blocks = vec![sample_block(10)];
534 let accumulator = Accumulator::new(B256::from([0xDD; 32]));
535 let block_index = DynamicBlockIndex::new(1000, 2, vec![100, 200]);
536
537 let mut group = EreGroup::new(blocks, Some(accumulator), block_index);
538 assert_eq!(group.other_entries.len(), 0);
539
540 group.add_entry(Entry::new([0x01, 0x01], vec![1, 2, 3, 4]));
541 group.add_entry(Entry::new([0x02, 0x02], vec![5, 6, 7, 8]));
542
543 assert_eq!(group.other_entries.len(), 2);
544 assert_eq!(group.other_entries[0].entry_type, [0x01, 0x01]);
545 assert_eq!(group.other_entries[1].data, vec![5, 6, 7, 8]);
546 }
547
548 #[test]
549 fn test_ere_group_with_mismatched_index() {
550 let blocks = vec![sample_block(10), sample_block(15)];
552 let index = DynamicBlockIndex::new(2000, 2, vec![100, 200, 300, 400, 500, 600]); let group = EreGroup::new(blocks, None, index);
554 assert_eq!(group.blocks.len(), 2);
555 assert_eq!(group.index.starting_number(), 2000);
556 }
557
558 #[test_case::test_case(
559 EreId::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]),
560 "mainnet-00000-5ec1ffb8.ere";
561 "Mainnet era 0"
562 )]
563 #[test_case::test_case(
564 EreId::new("mainnet", 8192, 8192).with_hash([0x5e, 0xcb, 0x9b, 0xf9]),
565 "mainnet-00001-5ecb9bf9.ere";
566 "Mainnet era 1"
567 )]
568 #[test_case::test_case(
569 EreId::new("sepolia", 0, 8192).with_hash([0x90, 0x91, 0x84, 0x72]),
570 "sepolia-00000-90918472.ere";
571 "Sepolia era 0"
572 )]
573 #[test_case::test_case(
574 EreId::new("mainnet", 1000, 100),
575 "mainnet-00000-00000000.ere";
576 "ID without hash"
577 )]
578 fn test_ere_id_file_naming(id: EreId, expected_file_name: &str) {
579 assert_eq!(id.to_file_name(), expected_file_name);
580 }
581
582 #[test_case::test_case(
584 EreId::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]).with_era_count(),
585 "mainnet-00000-00001-5ec1ffb8.ere";
586 "Mainnet era 0 with count"
587 )]
588 #[test_case::test_case(
589 EreId::new("mainnet", 8000, 500).with_hash([0xab, 0xcd, 0xef, 0x12]).with_era_count(),
590 "mainnet-00000-00002-abcdef12.ere";
591 "Spanning two eras with count"
592 )]
593 fn test_ere_id_file_naming_with_era_count(id: EreId, expected_file_name: &str) {
594 assert_eq!(id.to_file_name(), expected_file_name);
595 }
596
597 #[test_case::test_case(
599 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]),
600 "mainnet-00000-4bb7de2e.ere";
601 "Default profile, no postfix"
602 )]
603 #[test_case::test_case(
604 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profile(EreProfile::NoProofs),
605 "mainnet-00000-4bb7de2e-noproofs.ere";
606 "noproofs profile"
607 )]
608 #[test_case::test_case(
609 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profile(EreProfile::NoReceipts),
610 "mainnet-00000-4bb7de2e-noreceipts.ere";
611 "noreceipts profile"
612 )]
613 #[test_case::test_case(
614 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profiles([EreProfile::NoProofs, EreProfile::NoReceipts]),
615 "mainnet-00000-4bb7de2e-noproofs-noreceipts.ere";
616 "Combined profiles"
617 )]
618 #[test_case::test_case(
619 EreId::new("mainnet", 0, 8192).with_hash([0x4b, 0xb7, 0xde, 0x2e]).with_profile(EreProfile::NoReceipts).with_profile(EreProfile::NoProofs).with_profile(EreProfile::NoReceipts),
621 "mainnet-00000-4bb7de2e-noproofs-noreceipts.ere";
622 "Profiles normalized to alphabetical order"
623 )]
624 fn test_ere_id_file_naming_with_profiles(id: EreId, expected_file_name: &str) {
625 assert_eq!(id.to_file_name(), expected_file_name);
626 }
627}