1use crate::{
6 e2s_types::{E2sError, Entry},
7 execution_types::{Accumulator, BlockTuple},
8};
9use alloy_primitives::BlockNumber;
10
11pub const BLOCK_INDEX: [u8; 2] = [0x66, 0x32];
13
14#[derive(Debug)]
18pub struct Era1Group {
19 pub blocks: Vec<BlockTuple>,
21
22 pub other_entries: Vec<Entry>,
24
25 pub accumulator: Accumulator,
27
28 pub block_index: BlockIndex,
30}
31
32impl Era1Group {
33 pub const fn new(
35 blocks: Vec<BlockTuple>,
36 accumulator: Accumulator,
37 block_index: BlockIndex,
38 ) -> Self {
39 Self { blocks, accumulator, block_index, other_entries: Vec::new() }
40 }
41 pub fn add_entry(&mut self, entry: Entry) {
43 self.other_entries.push(entry);
44 }
45}
46
47#[derive(Debug, Clone)]
53pub struct BlockIndex {
54 pub starting_number: BlockNumber,
56
57 pub offsets: Vec<i64>,
59}
60
61impl BlockIndex {
62 pub const fn new(starting_number: BlockNumber, offsets: Vec<i64>) -> Self {
64 Self { starting_number, offsets }
65 }
66
67 pub fn offset_for_block(&self, block_number: BlockNumber) -> Option<i64> {
69 if block_number < self.starting_number {
70 return None;
71 }
72
73 let index = (block_number - self.starting_number) as usize;
74 self.offsets.get(index).copied()
75 }
76
77 pub fn to_entry(&self) -> Entry {
79 let mut data = Vec::with_capacity(8 + self.offsets.len() * 8 + 8);
81
82 data.extend_from_slice(&self.starting_number.to_le_bytes());
84
85 for offset in &self.offsets {
87 data.extend_from_slice(&offset.to_le_bytes());
88 }
89
90 data.extend_from_slice(&(self.offsets.len() as i64).to_le_bytes());
92
93 Entry::new(BLOCK_INDEX, data)
94 }
95
96 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
98 if entry.entry_type != BLOCK_INDEX {
99 return Err(E2sError::Ssz(format!(
100 "Invalid entry type for BlockIndex: expected {:02x}{:02x}, got {:02x}{:02x}",
101 BLOCK_INDEX[0], BLOCK_INDEX[1], entry.entry_type[0], entry.entry_type[1]
102 )));
103 }
104
105 if entry.data.len() < 16 {
106 return Err(E2sError::Ssz(String::from(
107 "BlockIndex entry too short to contain starting block number and count",
108 )));
109 }
110
111 let mut starting_number_bytes = [0u8; 8];
113 starting_number_bytes.copy_from_slice(&entry.data[0..8]);
114 let starting_number = u64::from_le_bytes(starting_number_bytes);
115
116 let mut count_bytes = [0u8; 8];
118 count_bytes.copy_from_slice(&entry.data[entry.data.len() - 8..]);
119 let count = u64::from_le_bytes(count_bytes) as usize;
120
121 let expected_size = 8 + count * 8 + 8;
123 if entry.data.len() != expected_size {
124 return Err(E2sError::Ssz(format!(
125 "BlockIndex entry has incorrect size: expected {}, got {}",
126 expected_size,
127 entry.data.len()
128 )));
129 }
130
131 let mut offsets = Vec::with_capacity(count);
133 for i in 0..count {
134 let start = 8 + i * 8;
135 let end = start + 8;
136 let mut offset_bytes = [0u8; 8];
137 offset_bytes.copy_from_slice(&entry.data[start..end]);
138 offsets.push(i64::from_le_bytes(offset_bytes));
139 }
140
141 Ok(Self { starting_number, offsets })
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct Era1Id {
148 pub network_name: String,
150
151 pub start_block: BlockNumber,
153
154 pub block_count: u32,
156
157 pub hash: Option<[u8; 4]>,
159}
160
161impl Era1Id {
162 pub fn new(
164 network_name: impl Into<String>,
165 start_block: BlockNumber,
166 block_count: u32,
167 ) -> Self {
168 Self { network_name: network_name.into(), start_block, block_count, hash: None }
169 }
170
171 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
173 self.hash = Some(hash);
174 self
175 }
176
177 pub fn to_file_name(&self) -> String {
183 if let Some(hash) = self.hash {
184 format!(
187 "{}-{:05}-{:02x}{:02x}{:02x}{:02x}.era1",
188 self.network_name, self.start_block, hash[0], hash[1], hash[2], hash[3]
189 )
190 } else {
191 format!("{}-{}-{}.era1", self.network_name, self.start_block, self.block_count)
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use crate::execution_types::{
201 CompressedBody, CompressedHeader, CompressedReceipts, TotalDifficulty,
202 };
203 use alloy_primitives::{B256, U256};
204
205 fn create_sample_block(data_size: usize) -> BlockTuple {
207 let header_data = vec![0xAA; data_size];
209 let header = CompressedHeader::new(header_data);
210
211 let body_data = vec![0xBB; data_size * 2];
213 let body = CompressedBody::new(body_data);
214
215 let receipts_data = vec![0xCC; data_size];
217 let receipts = CompressedReceipts::new(receipts_data);
218
219 let difficulty = TotalDifficulty::new(U256::from(data_size));
220
221 BlockTuple::new(header, body, receipts, difficulty)
223 }
224
225 #[test]
226 fn test_block_index_roundtrip() {
227 let starting_number = 1000;
228 let offsets = vec![100, 200, 300, 400, 500];
229
230 let block_index = BlockIndex::new(starting_number, offsets.clone());
231
232 let entry = block_index.to_entry();
233
234 assert_eq!(entry.entry_type, BLOCK_INDEX);
236
237 let recovered = BlockIndex::from_entry(&entry).unwrap();
239
240 assert_eq!(recovered.starting_number, starting_number);
242 assert_eq!(recovered.offsets, offsets);
243 }
244
245 #[test]
246 fn test_block_index_offset_lookup() {
247 let starting_number = 1000;
248 let offsets = vec![100, 200, 300, 400, 500];
249
250 let block_index = BlockIndex::new(starting_number, offsets);
251
252 assert_eq!(block_index.offset_for_block(1000), Some(100));
254 assert_eq!(block_index.offset_for_block(1002), Some(300));
255 assert_eq!(block_index.offset_for_block(1004), Some(500));
256
257 assert_eq!(block_index.offset_for_block(999), None);
259 assert_eq!(block_index.offset_for_block(1005), None);
260 }
261
262 #[test]
263 fn test_era1_group_basic_construction() {
264 let blocks =
265 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
266
267 let root_bytes = [0xDD; 32];
268 let accumulator = Accumulator::new(B256::from(root_bytes));
269 let block_index = BlockIndex::new(1000, vec![100, 200, 300]);
270
271 let era1_group = Era1Group::new(blocks, accumulator.clone(), block_index);
272
273 assert_eq!(era1_group.blocks.len(), 3);
275 assert_eq!(era1_group.other_entries.len(), 0);
276 assert_eq!(era1_group.accumulator.root, accumulator.root);
277 assert_eq!(era1_group.block_index.starting_number, 1000);
278 assert_eq!(era1_group.block_index.offsets, vec![100, 200, 300]);
279 }
280
281 #[test]
282 fn test_era1_group_add_entries() {
283 let blocks = vec![create_sample_block(10)];
284
285 let root_bytes = [0xDD; 32];
286 let accumulator = Accumulator::new(B256::from(root_bytes));
287
288 let block_index = BlockIndex::new(1000, vec![100]);
289
290 let mut era1_group = Era1Group::new(blocks, accumulator, block_index);
292 assert_eq!(era1_group.other_entries.len(), 0);
293
294 let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
296 let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
297
298 era1_group.add_entry(entry1);
300 era1_group.add_entry(entry2);
301
302 assert_eq!(era1_group.other_entries.len(), 2);
304 assert_eq!(era1_group.other_entries[0].entry_type, [0x01, 0x01]);
305 assert_eq!(era1_group.other_entries[0].data, vec![1, 2, 3, 4]);
306 assert_eq!(era1_group.other_entries[1].entry_type, [0x02, 0x02]);
307 assert_eq!(era1_group.other_entries[1].data, vec![5, 6, 7, 8]);
308 }
309
310 #[test]
311 fn test_era1_group_with_mismatched_index() {
312 let blocks =
313 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
314
315 let root_bytes = [0xDD; 32];
316 let accumulator = Accumulator::new(B256::from(root_bytes));
317
318 let block_index = BlockIndex::new(2000, vec![100, 200, 300]);
320
321 let era1_group = Era1Group::new(blocks, accumulator, block_index);
325
326 assert_eq!(era1_group.blocks.len(), 3);
328 assert_eq!(era1_group.block_index.starting_number, 2000);
329 }
330
331 #[test_case::test_case(
332 Era1Id::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]),
333 "mainnet-00000-5ec1ffb8.era1";
334 "Mainnet 00000"
335 )]
336 #[test_case::test_case(
337 Era1Id::new("mainnet", 12, 8192).with_hash([0x5e, 0xcb, 0x9b, 0xf9]),
338 "mainnet-00012-5ecb9bf9.era1";
339 "Mainnet 00012"
340 )]
341 #[test_case::test_case(
342 Era1Id::new("sepolia", 5, 8192).with_hash([0x90, 0x91, 0x84, 0x72]),
343 "sepolia-00005-90918472.era1";
344 "Sepolia 00005"
345 )]
346 #[test_case::test_case(
347 Era1Id::new("sepolia", 19, 8192).with_hash([0xfa, 0x77, 0x00, 0x19]),
348 "sepolia-00019-fa770019.era1";
349 "Sepolia 00019"
350 )]
351 #[test_case::test_case(
352 Era1Id::new("mainnet", 1000, 100),
353 "mainnet-1000-100.era1";
354 "ID without hash"
355 )]
356 #[test_case::test_case(
357 Era1Id::new("sepolia", 12345, 8192).with_hash([0xab, 0xcd, 0xef, 0x12]),
358 "sepolia-12345-abcdef12.era1";
359 "Large block number"
360 )]
361 fn test_era1id_file_naming(id: Era1Id, expected_file_name: &str) {
362 let actual_file_name = id.to_file_name();
363 assert_eq!(actual_file_name, expected_file_name);
364 }
365}