1use crate::{
6 common::file_ops::{EraFileId, EraFileType},
7 e2s::types::{Entry, IndexEntry},
8 era1::types::execution::{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 pub include_era_count: bool,
112}
113
114impl Era1Id {
115 pub fn new(
117 network_name: impl Into<String>,
118 start_block: BlockNumber,
119 block_count: u32,
120 ) -> Self {
121 Self {
122 network_name: network_name.into(),
123 start_block,
124 block_count,
125 hash: None,
126 include_era_count: false,
127 }
128 }
129
130 pub const fn with_hash(mut self, hash: [u8; 4]) -> Self {
132 self.hash = Some(hash);
133 self
134 }
135
136 pub const fn with_era_count(mut self) -> Self {
138 self.include_era_count = true;
139 self
140 }
141}
142
143impl EraFileId for Era1Id {
144 const FILE_TYPE: EraFileType = EraFileType::Era1;
145
146 const ITEMS_PER_ERA: u64 = MAX_BLOCKS_PER_ERA1 as u64;
147 fn network_name(&self) -> &str {
148 &self.network_name
149 }
150
151 fn start_number(&self) -> u64 {
152 self.start_block
153 }
154
155 fn count(&self) -> u32 {
156 self.block_count
157 }
158
159 fn hash(&self) -> Option<[u8; 4]> {
160 self.hash
161 }
162
163 fn include_era_count(&self) -> bool {
164 self.include_era_count
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::{
172 common::decode::DecodeCompressedRlp,
173 test_utils::{create_sample_block, create_test_block_with_compressed_data},
174 };
175 use alloy_consensus::ReceiptWithBloom;
176 use alloy_primitives::{B256, U256};
177
178 #[test]
179 fn test_alloy_components_decode_and_receipt_in_bloom() {
180 let block: BlockTuple = create_test_block_with_compressed_data(30);
182
183 let header: alloy_consensus::Header = block.header.decode().unwrap();
185 assert_eq!(header.number, 30, "Header block number should match");
186 assert_eq!(header.difficulty, U256::from(30 * 1000), "Header difficulty should match");
187 assert_eq!(header.gas_limit, 5000000, "Gas limit should match");
188 assert_eq!(header.gas_used, 21000, "Gas used should match");
189 assert_eq!(header.timestamp, 1609459200 + 30, "Timestamp should match");
190 assert_eq!(header.base_fee_per_gas, Some(10), "Base fee per gas should match");
191 assert!(header.withdrawals_root.is_some(), "Should have withdrawals root");
192 assert!(header.blob_gas_used.is_none(), "Should not have blob gas used");
193 assert!(header.excess_blob_gas.is_none(), "Should not have excess blob gas");
194
195 let body: alloy_consensus::BlockBody<alloy_primitives::Bytes> =
196 block.body.decode().unwrap();
197 assert_eq!(body.ommers.len(), 0, "Should have no ommers");
198 assert!(body.withdrawals.is_some(), "Should have withdrawals field");
199
200 let receipts: Vec<ReceiptWithBloom> = block.receipts.decode().unwrap();
201 assert_eq!(receipts.len(), 1, "Should have exactly 1 receipt");
202 }
203
204 #[test]
205 fn test_block_index_roundtrip() {
206 let starting_number = 1000;
207 let offsets = vec![100, 200, 300, 400, 500];
208
209 let block_index = BlockIndex::new(starting_number, offsets.clone());
210
211 let entry = block_index.to_entry();
212
213 assert_eq!(entry.entry_type, BLOCK_INDEX);
215
216 let recovered = BlockIndex::from_entry(&entry).unwrap();
218
219 assert_eq!(recovered.starting_number, starting_number);
221 assert_eq!(recovered.offsets, offsets);
222 }
223
224 #[test]
225 fn test_block_index_offset_lookup() {
226 let starting_number = 1000;
227 let offsets = vec![100, 200, 300, 400, 500];
228
229 let block_index = BlockIndex::new(starting_number, offsets);
230
231 assert_eq!(block_index.offset_for_block(1000), Some(100));
233 assert_eq!(block_index.offset_for_block(1002), Some(300));
234 assert_eq!(block_index.offset_for_block(1004), Some(500));
235
236 assert_eq!(block_index.offset_for_block(999), None);
238 assert_eq!(block_index.offset_for_block(1005), None);
239 }
240
241 #[test]
242 fn test_era1_group_basic_construction() {
243 let blocks =
244 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
245
246 let root_bytes = [0xDD; 32];
247 let accumulator = Accumulator::new(B256::from(root_bytes));
248 let block_index = BlockIndex::new(1000, vec![100, 200, 300]);
249
250 let era1_group = Era1Group::new(blocks, accumulator.clone(), block_index);
251
252 assert_eq!(era1_group.blocks.len(), 3);
254 assert_eq!(era1_group.other_entries.len(), 0);
255 assert_eq!(era1_group.accumulator.root, accumulator.root);
256 assert_eq!(era1_group.block_index.starting_number, 1000);
257 assert_eq!(era1_group.block_index.offsets, vec![100, 200, 300]);
258 }
259
260 #[test]
261 fn test_era1_group_add_entries() {
262 let blocks = vec![create_sample_block(10)];
263
264 let root_bytes = [0xDD; 32];
265 let accumulator = Accumulator::new(B256::from(root_bytes));
266
267 let block_index = BlockIndex::new(1000, vec![100]);
268
269 let mut era1_group = Era1Group::new(blocks, accumulator, block_index);
271 assert_eq!(era1_group.other_entries.len(), 0);
272
273 let entry1 = Entry::new([0x01, 0x01], vec![1, 2, 3, 4]);
275 let entry2 = Entry::new([0x02, 0x02], vec![5, 6, 7, 8]);
276
277 era1_group.add_entry(entry1);
279 era1_group.add_entry(entry2);
280
281 assert_eq!(era1_group.other_entries.len(), 2);
283 assert_eq!(era1_group.other_entries[0].entry_type, [0x01, 0x01]);
284 assert_eq!(era1_group.other_entries[0].data, vec![1, 2, 3, 4]);
285 assert_eq!(era1_group.other_entries[1].entry_type, [0x02, 0x02]);
286 assert_eq!(era1_group.other_entries[1].data, vec![5, 6, 7, 8]);
287 }
288
289 #[test]
290 fn test_era1_group_with_mismatched_index() {
291 let blocks =
292 vec![create_sample_block(10), create_sample_block(15), create_sample_block(20)];
293
294 let root_bytes = [0xDD; 32];
295 let accumulator = Accumulator::new(B256::from(root_bytes));
296
297 let block_index = BlockIndex::new(2000, vec![100, 200, 300]);
299
300 let era1_group = Era1Group::new(blocks, accumulator, block_index);
304
305 assert_eq!(era1_group.blocks.len(), 3);
307 assert_eq!(era1_group.block_index.starting_number, 2000);
308 }
309
310 #[test_case::test_case(
311 Era1Id::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]),
312 "mainnet-00000-5ec1ffb8.era1";
313 "Mainnet era 0"
314 )]
315 #[test_case::test_case(
316 Era1Id::new("mainnet", 8192, 8192).with_hash([0x5e, 0xcb, 0x9b, 0xf9]),
317 "mainnet-00001-5ecb9bf9.era1";
318 "Mainnet era 1"
319 )]
320 #[test_case::test_case(
321 Era1Id::new("sepolia", 0, 8192).with_hash([0x90, 0x91, 0x84, 0x72]),
322 "sepolia-00000-90918472.era1";
323 "Sepolia era 0"
324 )]
325 #[test_case::test_case(
326 Era1Id::new("sepolia", 155648, 8192).with_hash([0xfa, 0x77, 0x00, 0x19]),
327 "sepolia-00019-fa770019.era1";
328 "Sepolia era 19"
329 )]
330 #[test_case::test_case(
331 Era1Id::new("mainnet", 1000, 100),
332 "mainnet-00000-00000000.era1";
333 "ID without hash"
334 )]
335 #[test_case::test_case(
336 Era1Id::new("sepolia", 101130240, 8192).with_hash([0xab, 0xcd, 0xef, 0x12]),
337 "sepolia-12345-abcdef12.era1";
338 "Large block number era 12345"
339 )]
340 fn test_era1_id_file_naming(id: Era1Id, expected_file_name: &str) {
341 let actual_file_name = id.to_file_name();
342 assert_eq!(actual_file_name, expected_file_name);
343 }
344
345 #[test_case::test_case(
347 Era1Id::new("mainnet", 0, 8192).with_hash([0x5e, 0xc1, 0xff, 0xb8]).with_era_count(),
348 "mainnet-00000-00001-5ec1ffb8.era1";
349 "Mainnet era 0 with count"
350 )]
351 #[test_case::test_case(
352 Era1Id::new("mainnet", 8000, 500).with_hash([0xab, 0xcd, 0xef, 0x12]).with_era_count(),
353 "mainnet-00000-00002-abcdef12.era1";
354 "Spanning two eras with count"
355 )]
356 fn test_era1_id_file_naming_with_era_count(id: Era1Id, expected_file_name: &str) {
357 let actual_file_name = id.to_file_name();
358 assert_eq!(actual_file_name, expected_file_name);
359 }
360}