1use crate::{
6 e2s_types::{Entry, IndexEntry},
7 era_file_ops::EraFileId,
8 execution_types::{Accumulator, BlockTuple, MAX_BLOCKS_PER_ERA1},
9};
10use alloy_primitives::BlockNumber;
11
12pub const BLOCK_INDEX: [u8; 2] = [0x66, 0x32];
14
15#[derive(Debug)]
19pub struct Era1Group {
20 pub blocks: Vec<BlockTuple>,
22
23 pub other_entries: Vec<Entry>,
25
26 pub accumulator: Accumulator,
28
29 pub block_index: BlockIndex,
31}
32
33impl Era1Group {
34 pub const fn new(
36 blocks: Vec<BlockTuple>,
37 accumulator: Accumulator,
38 block_index: BlockIndex,
39 ) -> Self {
40 Self { blocks, accumulator, block_index, other_entries: Vec::new() }
41 }
42
43 pub fn add_entry(&mut self, entry: Entry) {
45 self.other_entries.push(entry);
46 }
47}
48
49#[derive(Debug, Clone)]
55pub struct BlockIndex {
56 starting_number: BlockNumber,
58
59 offsets: Vec<i64>,
61}
62
63impl BlockIndex {
64 pub fn offset_for_block(&self, block_number: BlockNumber) -> Option<i64> {
66 if block_number < self.starting_number {
67 return None;
68 }
69
70 let index = (block_number - self.starting_number) as usize;
71 self.offsets.get(index).copied()
72 }
73}
74
75impl IndexEntry for BlockIndex {
76 fn new(starting_number: u64, offsets: Vec<i64>) -> Self {
77 Self { starting_number, offsets }
78 }
79
80 fn entry_type() -> [u8; 2] {
81 BLOCK_INDEX
82 }
83
84 fn starting_number(&self) -> u64 {
85 self.starting_number
86 }
87
88 fn offsets(&self) -> &[i64] {
89 &self.offsets
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Era1Id {
96 pub network_name: String,
98
99 pub start_block: BlockNumber,
101
102 pub block_count: u32,
104
105 pub hash: Option<[u8; 4]>,
108}
109
110impl Era1Id {
111 pub fn new(
113 network_name: impl Into<String>,
114 start_block: BlockNumber,
115 block_count: u32,
116 ) -> Self {
117 Self { network_name: network_name.into(), start_block, block_count, hash: None }
118 }
119
120 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
122 self.hash = Some(hash);
123 self
124 }
125
126 const fn calculate_era_count(&self, first_era: u64) -> u64 {
131 let last_block = self.start_block + self.block_count as u64 - 1;
133 let last_era = last_block / MAX_BLOCKS_PER_ERA1 as u64;
135 last_era - first_era + 1
137 }
138}
139
140impl EraFileId for Era1Id {
141 fn network_name(&self) -> &str {
142 &self.network_name
143 }
144
145 fn start_number(&self) -> u64 {
146 self.start_block
147 }
148
149 fn count(&self) -> u32 {
150 self.block_count
151 }
152 fn to_file_name(&self) -> String {
157 let era_number = self.start_block / MAX_BLOCKS_PER_ERA1 as u64;
159 let era_count = self.calculate_era_count(era_number);
160 if let Some(hash) = self.hash {
161 format!(
162 "{}-{:05}-{:05}-{:02x}{:02x}{:02x}{:02x}.era1",
163 self.network_name, era_number, era_count, hash[0], hash[1], hash[2], hash[3]
164 )
165 } else {
166 format!("{}-{:05}-{:05}-00000000.era1", self.network_name, era_number, era_count)
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::{
177 test_utils::{create_sample_block, create_test_block_with_compressed_data},
178 DecodeCompressed,
179 };
180 use alloy_consensus::ReceiptWithBloom;
181 use alloy_primitives::{B256, U256};
182
183 #[test]
184 fn test_alloy_components_decode_and_receipt_in_bloom() {
185 let block: BlockTuple = create_test_block_with_compressed_data(30);
187
188 let header: alloy_consensus::Header = block.header.decode().unwrap();
190 assert_eq!(header.number, 30, "Header block number should match");
191 assert_eq!(header.difficulty, U256::from(30 * 1000), "Header difficulty should match");
192 assert_eq!(header.gas_limit, 5000000, "Gas limit should match");
193 assert_eq!(header.gas_used, 21000, "Gas used should match");
194 assert_eq!(header.timestamp, 1609459200 + 30, "Timestamp should match");
195 assert_eq!(header.base_fee_per_gas, Some(10), "Base fee per gas should match");
196 assert!(header.withdrawals_root.is_some(), "Should have withdrawals root");
197 assert!(header.blob_gas_used.is_none(), "Should not have blob gas used");
198 assert!(header.excess_blob_gas.is_none(), "Should not have excess blob gas");
199
200 let body: alloy_consensus::BlockBody<alloy_primitives::Bytes> =
201 block.body.decode().unwrap();
202 assert_eq!(body.ommers.len(), 0, "Should have no ommers");
203 assert!(body.withdrawals.is_some(), "Should have withdrawals field");
204
205 let receipts: Vec<ReceiptWithBloom> = block.receipts.decode().unwrap();
206 assert_eq!(receipts.len(), 1, "Should have exactly 1 receipt");
207 }
208
209 #[test]
210 fn test_block_index_roundtrip() {
211 let starting_number = 1000;
212 let offsets = vec![100, 200, 300, 400, 500];
213
214 let block_index = BlockIndex::new(starting_number, offsets.clone());
215
216 let entry = block_index.to_entry();
217
218 assert_eq!(entry.entry_type, BLOCK_INDEX);
220
221 let recovered = BlockIndex::from_entry(&entry).unwrap();
223
224 assert_eq!(recovered.starting_number, starting_number);
226 assert_eq!(recovered.offsets, offsets);
227 }
228
229 #[test]
230 fn test_block_index_offset_lookup() {
231 let starting_number = 1000;
232 let offsets = vec![100, 200, 300, 400, 500];
233
234 let block_index = BlockIndex::new(starting_number, offsets);
235
236 assert_eq!(block_index.offset_for_block(1000), Some(100));
238 assert_eq!(block_index.offset_for_block(1002), Some(300));
239 assert_eq!(block_index.offset_for_block(1004), Some(500));
240
241 assert_eq!(block_index.offset_for_block(999), None);
243 assert_eq!(block_index.offset_for_block(1005), None);
244 }
245
246 #[test]
247 fn test_era1_group_basic_construction() {
248 let blocks =
249 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
250
251 let root_bytes = [0xDD; 32];
252 let accumulator = Accumulator::new(B256::from(root_bytes));
253 let block_index = BlockIndex::new(1000, vec![100, 200, 300]);
254
255 let era1_group = Era1Group::new(blocks, accumulator.clone(), block_index);
256
257 assert_eq!(era1_group.blocks.len(), 3);
259 assert_eq!(era1_group.other_entries.len(), 0);
260 assert_eq!(era1_group.accumulator.root, accumulator.root);
261 assert_eq!(era1_group.block_index.starting_number, 1000);
262 assert_eq!(era1_group.block_index.offsets, vec![100, 200, 300]);
263 }
264
265 #[test]
266 fn test_era1_group_add_entries() {
267 let blocks = vec![create_sample_block(10)];
268
269 let root_bytes = [0xDD; 32];
270 let accumulator = Accumulator::new(B256::from(root_bytes));
271
272 let block_index = BlockIndex::new(1000, vec![100]);
273
274 let mut era1_group = Era1Group::new(blocks, accumulator, block_index);
276 assert_eq!(era1_group.other_entries.len(), 0);
277
278 let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
280 let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
281
282 era1_group.add_entry(entry1);
284 era1_group.add_entry(entry2);
285
286 assert_eq!(era1_group.other_entries.len(), 2);
288 assert_eq!(era1_group.other_entries[0].entry_type, [0x01, 0x01]);
289 assert_eq!(era1_group.other_entries[0].data, vec![1, 2, 3, 4]);
290 assert_eq!(era1_group.other_entries[1].entry_type, [0x02, 0x02]);
291 assert_eq!(era1_group.other_entries[1].data, vec![5, 6, 7, 8]);
292 }
293
294 #[test]
295 fn test_era1_group_with_mismatched_index() {
296 let blocks =
297 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
298
299 let root_bytes = [0xDD; 32];
300 let accumulator = Accumulator::new(B256::from(root_bytes));
301
302 let block_index = BlockIndex::new(2000, vec![100, 200, 300]);
304
305 let era1_group = Era1Group::new(blocks, accumulator, block_index);
309
310 assert_eq!(era1_group.blocks.len(), 3);
312 assert_eq!(era1_group.block_index.starting_number, 2000);
313 }
314
315 #[test_case::test_case(
316 Era1Id::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]),
317 "mainnet-00000-00001-5ec1ffb8.era1";
318 "Mainnet era 0"
319 )]
320 #[test_case::test_case(
321 Era1Id::new("mainnet", 8192, 8192).with_hash([0x5e, 0xcb, 0x9b, 0xf9]),
322 "mainnet-00001-00001-5ecb9bf9.era1";
323 "Mainnet era 1"
324 )]
325 #[test_case::test_case(
326 Era1Id::new("sepolia", 0, 8192).with_hash([0x90, 0x91, 0x84, 0x72]),
327 "sepolia-00000-00001-90918472.era1";
328 "Sepolia era 0"
329 )]
330 #[test_case::test_case(
331 Era1Id::new("sepolia", 155648, 8192).with_hash([0xfa, 0x77, 0x00, 0x19]),
332 "sepolia-00019-00001-fa770019.era1";
333 "Sepolia era 19"
334 )]
335 #[test_case::test_case(
336 Era1Id::new("mainnet", 1000, 100),
337 "mainnet-00000-00001-00000000.era1";
338 "ID without hash"
339 )]
340 #[test_case::test_case(
341 Era1Id::new("sepolia", 101130240, 8192).with_hash([0xab, 0xcd, 0xef, 0x12]),
342 "sepolia-12345-00001-abcdef12.era1";
343 "Large block number era 12345"
344 )]
345 fn test_era1id_file_naming(id: Era1Id, expected_file_name: &str) {
346 let actual_file_name = id.to_file_name();
347 assert_eq!(actual_file_name, expected_file_name);
348 }
349}