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 fn new(blocks: Vec<BlockTuple>, accumulator: Accumulator, block_index: BlockIndex) -> Self {
35 Self { blocks, accumulator, block_index, other_entries: Vec::new() }
36 }
37 pub fn add_entry(&mut self, entry: Entry) {
39 self.other_entries.push(entry);
40 }
41}
42
43#[derive(Debug, Clone)]
49pub struct BlockIndex {
50 pub starting_number: BlockNumber,
52
53 pub offsets: Vec<i64>,
55}
56
57impl BlockIndex {
58 pub fn new(starting_number: BlockNumber, offsets: Vec<i64>) -> Self {
60 Self { starting_number, offsets }
61 }
62
63 pub fn offset_for_block(&self, block_number: BlockNumber) -> Option<i64> {
65 if block_number < self.starting_number {
66 return None;
67 }
68
69 let index = (block_number - self.starting_number) as usize;
70 self.offsets.get(index).copied()
71 }
72
73 pub fn to_entry(&self) -> Entry {
75 let mut data = Vec::with_capacity(8 + self.offsets.len() * 8 + 8);
77
78 data.extend_from_slice(&self.starting_number.to_le_bytes());
80
81 for offset in &self.offsets {
83 data.extend_from_slice(&offset.to_le_bytes());
84 }
85
86 data.extend_from_slice(&(self.offsets.len() as i64).to_le_bytes());
88
89 Entry::new(BLOCK_INDEX, data)
90 }
91
92 pub fn from_entry(entry: &Entry) -> Result<Self, E2sError> {
94 if entry.entry_type != BLOCK_INDEX {
95 return Err(E2sError::Ssz(format!(
96 "Invalid entry type for BlockIndex: expected {:02x}{:02x}, got {:02x}{:02x}",
97 BLOCK_INDEX[0], BLOCK_INDEX[1], entry.entry_type[0], entry.entry_type[1]
98 )));
99 }
100
101 if entry.data.len() < 16 {
102 return Err(E2sError::Ssz(String::from(
103 "BlockIndex entry too short to contain starting block number and count",
104 )));
105 }
106
107 let mut starting_number_bytes = [0u8; 8];
109 starting_number_bytes.copy_from_slice(&entry.data[0..8]);
110 let starting_number = u64::from_le_bytes(starting_number_bytes);
111
112 let mut count_bytes = [0u8; 8];
114 count_bytes.copy_from_slice(&entry.data[entry.data.len() - 8..]);
115 let count = u64::from_le_bytes(count_bytes) as usize;
116
117 let expected_size = 8 + count * 8 + 8;
119 if entry.data.len() != expected_size {
120 return Err(E2sError::Ssz(format!(
121 "BlockIndex entry has incorrect size: expected {}, got {}",
122 expected_size,
123 entry.data.len()
124 )));
125 }
126
127 let mut offsets = Vec::with_capacity(count);
129 for i in 0..count {
130 let start = 8 + i * 8;
131 let end = start + 8;
132 let mut offset_bytes = [0u8; 8];
133 offset_bytes.copy_from_slice(&entry.data[start..end]);
134 offsets.push(i64::from_le_bytes(offset_bytes));
135 }
136
137 Ok(Self { starting_number, offsets })
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct Era1Id {
144 pub network_name: String,
146
147 pub start_block: BlockNumber,
149
150 pub block_count: u32,
152
153 pub hash: Option<[u8; 4]>,
155}
156
157impl Era1Id {
158 pub fn new(
160 network_name: impl Into<String>,
161 start_block: BlockNumber,
162 block_count: u32,
163 ) -> Self {
164 Self { network_name: network_name.into(), start_block, block_count, hash: None }
165 }
166
167 pub fn with_hash(mut self, hash: [u8; 4]) -> Self {
169 self.hash = Some(hash);
170 self
171 }
172
173 pub fn to_file_name(&self) -> String {
179 if let Some(hash) = self.hash {
180 format!(
183 "{}-{:05}-{:02x}{:02x}{:02x}{:02x}.era1",
184 self.network_name, self.start_block, hash[0], hash[1], hash[2], hash[3]
185 )
186 } else {
187 format!("{}-{}-{}.era1", self.network_name, self.start_block, self.block_count)
189 }
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196 use crate::execution_types::{
197 CompressedBody, CompressedHeader, CompressedReceipts, TotalDifficulty,
198 };
199 use alloy_primitives::{B256, U256};
200
201 fn create_sample_block(data_size: usize) -> BlockTuple {
203 let header_data = vec![0xAA; data_size];
205 let header = CompressedHeader::new(header_data);
206
207 let body_data = vec![0xBB; data_size * 2];
209 let body = CompressedBody::new(body_data);
210
211 let receipts_data = vec![0xCC; data_size];
213 let receipts = CompressedReceipts::new(receipts_data);
214
215 let difficulty = TotalDifficulty::new(U256::from(data_size));
216
217 BlockTuple::new(header, body, receipts, difficulty)
219 }
220
221 #[test]
222 fn test_block_index_roundtrip() {
223 let starting_number = 1000;
224 let offsets = vec![100, 200, 300, 400, 500];
225
226 let block_index = BlockIndex::new(starting_number, offsets.clone());
227
228 let entry = block_index.to_entry();
229
230 assert_eq!(entry.entry_type, BLOCK_INDEX);
232
233 let recovered = BlockIndex::from_entry(&entry).unwrap();
235
236 assert_eq!(recovered.starting_number, starting_number);
238 assert_eq!(recovered.offsets, offsets);
239 }
240
241 #[test]
242 fn test_block_index_offset_lookup() {
243 let starting_number = 1000;
244 let offsets = vec![100, 200, 300, 400, 500];
245
246 let block_index = BlockIndex::new(starting_number, offsets);
247
248 assert_eq!(block_index.offset_for_block(1000), Some(100));
250 assert_eq!(block_index.offset_for_block(1002), Some(300));
251 assert_eq!(block_index.offset_for_block(1004), Some(500));
252
253 assert_eq!(block_index.offset_for_block(999), None);
255 assert_eq!(block_index.offset_for_block(1005), None);
256 }
257
258 #[test]
259 fn test_era1_group_basic_construction() {
260 let blocks =
261 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
262
263 let root_bytes = [0xDD; 32];
264 let accumulator = Accumulator::new(B256::from(root_bytes));
265 let block_index = BlockIndex::new(1000, vec![100, 200, 300]);
266
267 let era1_group = Era1Group::new(blocks, accumulator.clone(), block_index);
268
269 assert_eq!(era1_group.blocks.len(), 3);
271 assert_eq!(era1_group.other_entries.len(), 0);
272 assert_eq!(era1_group.accumulator.root, accumulator.root);
273 assert_eq!(era1_group.block_index.starting_number, 1000);
274 assert_eq!(era1_group.block_index.offsets, vec![100, 200, 300]);
275 }
276
277 #[test]
278 fn test_era1_group_add_entries() {
279 let blocks = vec![create_sample_block(10)];
280
281 let root_bytes = [0xDD; 32];
282 let accumulator = Accumulator::new(B256::from(root_bytes));
283
284 let block_index = BlockIndex::new(1000, vec![100]);
285
286 let mut era1_group = Era1Group::new(blocks, accumulator, block_index);
288 assert_eq!(era1_group.other_entries.len(), 0);
289
290 let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
292 let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
293
294 era1_group.add_entry(entry1);
296 era1_group.add_entry(entry2);
297
298 assert_eq!(era1_group.other_entries.len(), 2);
300 assert_eq!(era1_group.other_entries[0].entry_type, [0x01, 0x01]);
301 assert_eq!(era1_group.other_entries[0].data, vec![1, 2, 3, 4]);
302 assert_eq!(era1_group.other_entries[1].entry_type, [0x02, 0x02]);
303 assert_eq!(era1_group.other_entries[1].data, vec![5, 6, 7, 8]);
304 }
305
306 #[test]
307 fn test_era1_group_with_mismatched_index() {
308 let blocks =
309 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
310
311 let root_bytes = [0xDD; 32];
312 let accumulator = Accumulator::new(B256::from(root_bytes));
313
314 let block_index = BlockIndex::new(2000, vec![100, 200, 300]);
316
317 let era1_group = Era1Group::new(blocks, accumulator, block_index);
321
322 assert_eq!(era1_group.blocks.len(), 3);
324 assert_eq!(era1_group.block_index.starting_number, 2000);
325 }
326
327 #[test]
328 fn test_era1id_file_naming() {
329 let mainnet_00000 = Era1Id::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]);
332 assert_eq!(mainnet_00000.to_file_name(), "mainnet-00000-5ec1ffb8.era1");
333
334 let mainnet_00012 = Era1Id::new("mainnet", 12, 8192).with_hash([0x5e, 0xcb, 0x9b, 0xf9]);
335 assert_eq!(mainnet_00012.to_file_name(), "mainnet-00012-5ecb9bf9.era1");
336
337 let sepolia_00005 = Era1Id::new("sepolia", 5, 8192).with_hash([0x90, 0x91, 0x84, 0x72]);
340 assert_eq!(sepolia_00005.to_file_name(), "sepolia-00005-90918472.era1");
341
342 let sepolia_00019 = Era1Id::new("sepolia", 19, 8192).with_hash([0xfa, 0x77, 0x00, 0x19]);
343 assert_eq!(sepolia_00019.to_file_name(), "sepolia-00019-fa770019.era1");
344
345 let id_without_hash = Era1Id::new("mainnet", 1000, 100);
347 assert_eq!(id_without_hash.to_file_name(), "mainnet-1000-100.era1");
348
349 let large_era = Era1Id::new("sepolia", 12345, 8192).with_hash([0xab, 0xcd, 0xef, 0x12]);
351 assert_eq!(large_era.to_file_name(), "sepolia-12345-abcdef12.era1");
352 }
353}