1mod manager;
2pub use manager::{
3 StaticFileAccess, StaticFileProvider, StaticFileProviderBuilder, StaticFileWriteCtx,
4 StaticFileWriter,
5};
6
7mod jar;
8pub use jar::StaticFileJarProvider;
9
10mod writer;
11pub use writer::{StaticFileProviderRW, StaticFileProviderRWRefMut};
12
13mod metrics;
14
15#[cfg(test)]
16mod writer_tests;
17
18use reth_nippy_jar::NippyJar;
19use reth_static_file_types::{ChangesetOffsetReader, SegmentHeader, StaticFileSegment};
20use reth_storage_errors::provider::{ProviderError, ProviderResult};
21use std::{io, ops::Deref, sync::Arc};
22
23type LoadedJarRef<'a> =
25 reth_primitives_traits::dashmap::mapref::one::Ref<'a, (u64, StaticFileSegment), LoadedJar>;
26
27#[derive(Debug)]
29pub struct LoadedJar {
30 jar: NippyJar<SegmentHeader>,
31 mmap_handle: Arc<reth_nippy_jar::DataReader>,
32 csoff_reader: Option<ChangesetOffsetReader>,
33}
34
35impl LoadedJar {
36 fn new(jar: NippyJar<SegmentHeader>) -> ProviderResult<Self> {
37 match jar.open_data_reader() {
38 Ok(data_reader) => {
39 let mmap_handle = Arc::new(data_reader);
40
41 let csoff_reader = if jar.user_header().segment().is_change_based() {
42 let csoff_path = jar.data_path().with_extension("csoff");
43 let len = jar.user_header().changeset_offsets_len();
44 match ChangesetOffsetReader::new(&csoff_path, len) {
45 Ok(reader) => Some(reader),
46 Err(err) if err.kind() == io::ErrorKind::NotFound && len == 0 => None,
47 Err(err) => return Err(ProviderError::other(err)),
48 }
49 } else {
50 None
51 };
52
53 Ok(Self { jar, mmap_handle, csoff_reader })
54 }
55 Err(e) => Err(ProviderError::other(e)),
56 }
57 }
58
59 fn mmap_handle(&self) -> Arc<reth_nippy_jar::DataReader> {
61 self.mmap_handle.clone()
62 }
63
64 const fn segment(&self) -> StaticFileSegment {
65 self.jar.user_header().segment()
66 }
67
68 fn size(&self) -> usize {
70 self.mmap_handle.size() + self.mmap_handle.offsets_size()
71 }
72
73 const fn csoff_reader(&self) -> Option<&ChangesetOffsetReader> {
75 self.csoff_reader.as_ref()
76 }
77}
78
79impl Deref for LoadedJar {
80 type Target = NippyJar<SegmentHeader>;
81 fn deref(&self) -> &Self::Target {
82 &self.jar
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use crate::{
90 providers::static_file::manager::StaticFileProviderBuilder,
91 test_utils::create_test_provider_factory, HeaderProvider, StaticFileProviderFactory,
92 };
93 use alloy_consensus::{Header, SignableTransaction, Transaction, TxLegacy};
94 use alloy_primitives::{Address, BlockHash, Signature, TxNumber, B256, U160, U256};
95 use rand::seq::SliceRandom;
96 use reth_db::{
97 models::{AccountBeforeTx, StorageBeforeTx},
98 test_utils::create_test_static_files_dir,
99 };
100 use reth_db_api::{transaction::DbTxMut, CanonicalHeaders, HeaderNumbers, Headers};
101 use reth_ethereum_primitives::{EthPrimitives, Receipt, TransactionSigned};
102 use reth_primitives_traits::Account;
103 use reth_static_file_types::{
104 find_fixed_range, SegmentRangeInclusive, DEFAULT_BLOCKS_PER_STATIC_FILE,
105 };
106 use reth_storage_api::{
107 ChangeSetReader, ReceiptProvider, StorageChangeSetReader, TransactionsProvider,
108 };
109 use reth_testing_utils::generators::{self, random_header_range};
110 use std::{collections::BTreeMap, fmt::Debug, fs, ops::Range, path::Path};
111
112 fn assert_eyre<T: PartialEq + Debug>(got: T, expected: T, msg: &str) -> eyre::Result<()> {
113 if got != expected {
114 eyre::bail!("{msg} | got: {got:?} expected: {expected:?}");
115 }
116 Ok(())
117 }
118
119 #[test]
120 fn test_static_files() {
121 let row_count = 100u64;
123 let range = 0..=(row_count - 1);
124
125 let factory = create_test_provider_factory();
127 let static_files_path = tempfile::tempdir().unwrap();
128 let static_file = static_files_path.path().join(
129 StaticFileSegment::Headers
130 .filename(&find_fixed_range(*range.end(), DEFAULT_BLOCKS_PER_STATIC_FILE)),
131 );
132
133 let mut headers = random_header_range(
135 &mut generators::rng(),
136 *range.start()..(*range.end() + 1),
137 B256::random(),
138 );
139
140 let mut provider_rw = factory.provider_rw().unwrap();
141 let tx = provider_rw.tx_mut();
142 for header in headers.clone() {
143 let hash = header.hash();
144
145 tx.put::<CanonicalHeaders>(header.number, hash).unwrap();
146 tx.put::<Headers>(header.number, header.clone_header()).unwrap();
147 tx.put::<HeaderNumbers>(hash, header.number).unwrap();
148 }
149 provider_rw.commit().unwrap();
150
151 {
153 let manager = factory.static_file_provider();
154 let mut writer = manager.latest_writer(StaticFileSegment::Headers).unwrap();
155
156 for header in headers.clone() {
157 let hash = header.hash();
158 writer.append_header(&header.unseal(), &hash).unwrap();
159 }
160 writer.commit().unwrap();
161 }
162
163 {
165 let db_provider = factory.provider().unwrap();
166 let manager = db_provider.static_file_provider();
167 let jar_provider = manager
168 .get_segment_provider_for_block(StaticFileSegment::Headers, 0, Some(&static_file))
169 .unwrap();
170
171 assert!(!headers.is_empty());
172
173 headers.shuffle(&mut generators::rng());
175
176 for header in headers {
177 let header_hash = header.hash();
178 let header = header.unseal();
179
180 assert_eq!(header, db_provider.header(header_hash).unwrap().unwrap());
182 assert_eq!(header, jar_provider.header_by_number(header.number).unwrap().unwrap());
183 }
184 }
185 }
186
187 #[test]
188 fn test_header_truncation() {
189 let (static_dir, _) = create_test_static_files_dir();
190
191 let blocks_per_file = 10; let files_per_range = 3; let file_set_count = 3; let initial_file_count = files_per_range * file_set_count;
195 let tip = blocks_per_file * file_set_count - 1; {
199 let sf_rw: StaticFileProvider<EthPrimitives> =
200 StaticFileProviderBuilder::read_write(&static_dir)
201 .with_blocks_per_file(blocks_per_file)
202 .build()
203 .expect("Failed to build static file provider");
204
205 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
206
207 let mut header = Header::default();
209 for num in 0..=tip {
210 header.number = num;
211 header_writer.append_header(&header, &BlockHash::default()).unwrap();
212 }
213 header_writer.commit().unwrap();
214 }
215
216 fn prune_and_validate(
218 writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
219 sf_rw: &StaticFileProvider<EthPrimitives>,
220 static_dir: impl AsRef<Path>,
221 prune_count: u64,
222 expected_tip: Option<u64>,
223 expected_file_count: u64,
224 ) -> eyre::Result<()> {
225 writer.prune_headers(prune_count)?;
226 writer.commit()?;
227
228 assert_eyre(
230 sf_rw.get_highest_static_file_block(StaticFileSegment::Headers),
231 expected_tip,
232 "block mismatch",
233 )?;
234
235 if let Some(id) = expected_tip {
236 assert_eyre(
237 sf_rw.header_by_number(id)?.map(|h| h.number),
238 expected_tip,
239 "header mismatch",
240 )?;
241 }
242
243 assert_eyre(
245 count_files_without_lockfile(static_dir)?,
246 expected_file_count as usize,
247 "file count mismatch",
248 )?;
249
250 Ok(())
251 }
252
253 type PruneCount = u64;
255 type ExpectedTip = u64;
256 type ExpectedFileCount = u64;
257 let mut tmp_tip = tip;
258 let test_cases: Vec<(PruneCount, Option<ExpectedTip>, ExpectedFileCount)> = vec![
259 {
261 tmp_tip -= 1;
262 (1, Some(tmp_tip), initial_file_count)
263 },
264 {
266 tmp_tip -= blocks_per_file - 1;
267 (blocks_per_file - 1, Some(tmp_tip), initial_file_count - files_per_range)
268 },
269 {
272 tmp_tip -= blocks_per_file + 1;
273 (blocks_per_file + 1, Some(tmp_tip), initial_file_count - files_per_range * 2)
274 },
275 {
277 (
278 tmp_tip,
279 Some(0), files_per_range, )
282 },
283 {
285 (
286 1,
287 None, files_per_range, )
290 },
291 ];
292
293 {
295 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
296 .with_blocks_per_file(blocks_per_file)
297 .build()
298 .expect("Failed to build static file provider");
299
300 assert_eq!(sf_rw.get_highest_static_file_block(StaticFileSegment::Headers), Some(tip));
301 assert_eq!(
302 count_files_without_lockfile(static_dir.as_ref()).unwrap(),
303 initial_file_count as usize
304 );
305
306 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
307
308 for (case, (prune_count, expected_tip, expected_file_count)) in
309 test_cases.into_iter().enumerate()
310 {
311 prune_and_validate(
312 &mut header_writer,
313 &sf_rw,
314 &static_dir,
315 prune_count,
316 expected_tip,
317 expected_file_count,
318 )
319 .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
320 .unwrap();
321 }
322 }
323 }
324
325 fn setup_tx_based_scenario(
332 sf_rw: &StaticFileProvider<EthPrimitives>,
333 segment: StaticFileSegment,
334 blocks_per_file: u64,
335 ) {
336 fn setup_block_ranges(
337 writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
338 sf_rw: &StaticFileProvider<EthPrimitives>,
339 segment: StaticFileSegment,
340 block_range: &Range<u64>,
341 mut tx_count: u64,
342 next_tx_num: &mut u64,
343 ) {
344 let mut receipt = Receipt::default();
345 let mut tx = TxLegacy::default();
346
347 for block in block_range.clone() {
348 writer.increment_block(block).unwrap();
349
350 if tx_count > 0 {
352 match segment {
353 StaticFileSegment::Headers |
354 StaticFileSegment::AccountChangeSets |
355 StaticFileSegment::StorageChangeSets => {
356 panic!("non tx based segment")
357 }
358 StaticFileSegment::Transactions => {
359 tx.nonce = *next_tx_num;
361 let tx: TransactionSigned =
362 tx.clone().into_signed(Signature::test_signature()).into();
363 writer.append_transaction(*next_tx_num, &tx).unwrap();
364 }
365 StaticFileSegment::Receipts => {
366 receipt.cumulative_gas_used = *next_tx_num;
368 writer.append_receipt(*next_tx_num, &receipt).unwrap();
369 }
370 StaticFileSegment::TransactionSenders => {
371 let sender = Address::from(U160::from(*next_tx_num));
373 writer.append_transaction_sender(*next_tx_num, &sender).unwrap();
374 }
375 }
376 *next_tx_num += 1;
377 tx_count -= 1;
378 }
379 }
380 writer.commit().unwrap();
381
382 let expected_block = block_range.end - 1;
384 let expected_tx = if tx_count == 0 { *next_tx_num - 1 } else { *next_tx_num };
385
386 assert_eq!(sf_rw.get_highest_static_file_block(segment), Some(expected_block),);
388 assert_eq!(sf_rw.get_highest_static_file_tx(segment), Some(expected_tx),);
389 }
390
391 let block_ranges = [
393 0..blocks_per_file,
394 blocks_per_file..blocks_per_file * 2,
395 blocks_per_file * 2..blocks_per_file * 3,
396 ];
397
398 let tx_counts = [
399 blocks_per_file - 1, 0, 1, ];
403
404 let mut writer = sf_rw.latest_writer(segment).unwrap();
405 let mut next_tx_num = 0;
406
407 for (block_range, tx_count) in block_ranges.iter().zip(tx_counts.iter()) {
409 setup_block_ranges(
410 &mut writer,
411 sf_rw,
412 segment,
413 block_range,
414 *tx_count,
415 &mut next_tx_num,
416 );
417 }
418
419 let expected_tx_ranges = vec![
421 Some(SegmentRangeInclusive::new(0, 8)),
422 None,
423 Some(SegmentRangeInclusive::new(9, 9)),
424 ];
425
426 block_ranges.iter().zip(expected_tx_ranges).for_each(|(block_range, expected_tx_range)| {
427 assert_eq!(
428 sf_rw
429 .get_segment_provider_for_block(segment, block_range.start, None)
430 .unwrap()
431 .user_header()
432 .tx_range(),
433 expected_tx_range
434 );
435 });
436
437 let expected_tx_index = BTreeMap::from([
439 (8, SegmentRangeInclusive::new(0, 9)),
440 (9, SegmentRangeInclusive::new(20, 29)),
441 ]);
442 assert_eq!(
443 sf_rw.tx_index(segment),
444 (!expected_tx_index.is_empty()).then_some(expected_tx_index),
445 "tx index mismatch",
446 );
447 }
448
449 #[test]
450 fn test_tx_based_truncation() {
451 let segments = [StaticFileSegment::Transactions, StaticFileSegment::Receipts];
452 let blocks_per_file = 10; let files_per_range = 3; let file_set_count = 3; let initial_file_count = files_per_range * file_set_count;
456
457 #[expect(clippy::too_many_arguments)]
458 fn prune_and_validate(
459 sf_rw: &StaticFileProvider<EthPrimitives>,
460 static_dir: impl AsRef<Path>,
461 segment: StaticFileSegment,
462 prune_count: u64,
463 last_block: u64,
464 expected_tx_tip: Option<u64>,
465 expected_file_count: i32,
466 expected_tx_index: BTreeMap<TxNumber, SegmentRangeInclusive>,
467 ) -> eyre::Result<()> {
468 let mut writer = sf_rw.latest_writer(segment)?;
469
470 match segment {
472 StaticFileSegment::Headers |
473 StaticFileSegment::AccountChangeSets |
474 StaticFileSegment::StorageChangeSets => {
475 panic!("non tx based segment")
476 }
477 StaticFileSegment::Transactions => {
478 writer.prune_transactions(prune_count, last_block)?
479 }
480 StaticFileSegment::Receipts => writer.prune_receipts(prune_count, last_block)?,
481 StaticFileSegment::TransactionSenders => {
482 writer.prune_transaction_senders(prune_count, last_block)?
483 }
484 }
485 writer.commit()?;
486
487 assert_eyre(
489 sf_rw.get_highest_static_file_block(segment),
490 Some(last_block),
491 "block mismatch",
492 )?;
493 assert_eyre(sf_rw.get_highest_static_file_tx(segment), expected_tx_tip, "tx mismatch")?;
494
495 if let Some(id) = expected_tx_tip {
498 match segment {
499 StaticFileSegment::Headers |
500 StaticFileSegment::AccountChangeSets |
501 StaticFileSegment::StorageChangeSets => {
502 panic!("non tx based segment")
503 }
504 StaticFileSegment::Transactions => assert_eyre(
505 expected_tx_tip,
506 sf_rw.transaction_by_id(id)?.map(|t| t.nonce()),
507 "tx mismatch",
508 )?,
509 StaticFileSegment::Receipts => assert_eyre(
510 expected_tx_tip,
511 sf_rw.receipt(id)?.map(|r| r.cumulative_gas_used),
512 "receipt mismatch",
513 )?,
514 StaticFileSegment::TransactionSenders => assert_eyre(
515 expected_tx_tip,
516 sf_rw
517 .transaction_sender(id)?
518 .map(|s| u64::try_from(U160::from_be_bytes(s.0.into())).unwrap()),
519 "sender mismatch",
520 )?,
521 }
522 }
523
524 assert_eyre(
526 count_files_without_lockfile(static_dir)?,
527 expected_file_count as usize,
528 "file count mismatch",
529 )?;
530
531 assert_eyre(
533 sf_rw.tx_index(segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()),
534 (!expected_tx_index.is_empty()).then_some(expected_tx_index),
535 "tx index mismatch",
536 )?;
537
538 Ok(())
539 }
540
541 for segment in segments {
542 let (static_dir, _) = create_test_static_files_dir();
543
544 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
545 .with_blocks_per_file(blocks_per_file)
546 .build()
547 .expect("Failed to build static file provider");
548
549 setup_tx_based_scenario(&sf_rw, segment, blocks_per_file);
550
551 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
552 .with_blocks_per_file(blocks_per_file)
553 .build()
554 .expect("Failed to build static file provider");
555 let highest_tx = sf_rw.get_highest_static_file_tx(segment).unwrap();
556
557 let test_cases = vec![
560 (
565 1,
566 blocks_per_file * 2,
567 Some(highest_tx - 1),
568 initial_file_count,
569 BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
570 ),
571 (
575 0,
576 blocks_per_file - 1,
577 Some(highest_tx - 1),
578 files_per_range,
579 BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
580 ),
581 (
583 highest_tx - 1,
584 1,
585 Some(0),
586 files_per_range,
587 BTreeMap::from([(0, SegmentRangeInclusive::new(0, 1))]),
588 ),
589 (1, 0, None, files_per_range, BTreeMap::from([])),
591 ];
592
593 for (
595 case,
596 (prune_count, last_block, expected_tx_tip, expected_file_count, expected_tx_index),
597 ) in test_cases.into_iter().enumerate()
598 {
599 prune_and_validate(
600 &sf_rw,
601 &static_dir,
602 segment,
603 prune_count,
604 last_block,
605 expected_tx_tip,
606 expected_file_count,
607 expected_tx_index,
608 )
609 .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
610 .unwrap();
611 }
612 }
613 }
614
615 fn count_files_without_lockfile(path: impl AsRef<Path>) -> eyre::Result<usize> {
617 let is_lockfile = |entry: &fs::DirEntry| {
618 entry.path().file_name().map(|name| name == "lock").unwrap_or(false)
619 };
620 let count = fs::read_dir(path)?
621 .filter_map(|entry| entry.ok())
622 .filter(|entry| !is_lockfile(entry))
623 .count();
624
625 Ok(count)
626 }
627
628 #[test]
629 fn test_dynamic_size() -> eyre::Result<()> {
630 let (static_dir, _) = create_test_static_files_dir();
631
632 {
633 let sf_rw: StaticFileProvider<EthPrimitives> =
634 StaticFileProviderBuilder::read_write(&static_dir)
635 .with_blocks_per_file(10)
636 .build()?;
637 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
638
639 let mut header = Header::default();
640 for num in 0..=15 {
641 header.number = num;
642 header_writer.append_header(&header, &BlockHash::default()).unwrap();
643 }
644 header_writer.commit().unwrap();
645
646 assert_eq!(sf_rw.headers_range(0..=15)?.len(), 16);
647 assert_eq!(
648 sf_rw.expected_block_index(StaticFileSegment::Headers),
649 Some(BTreeMap::from([
650 (9, SegmentRangeInclusive::new(0, 9)),
651 (19, SegmentRangeInclusive::new(10, 19))
652 ])),
653 )
654 }
655
656 {
657 let sf_rw: StaticFileProvider<EthPrimitives> =
658 StaticFileProviderBuilder::read_write(&static_dir)
659 .with_blocks_per_file(5)
660 .build()?;
661 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
662
663 let mut header = Header::default();
664 for num in 16..=22 {
665 header.number = num;
666 header_writer.append_header(&header, &BlockHash::default()).unwrap();
667 }
668 header_writer.commit().unwrap();
669
670 assert_eq!(sf_rw.headers_range(0..=22)?.len(), 23);
671 assert_eq!(
672 sf_rw.expected_block_index(StaticFileSegment::Headers),
673 Some(BTreeMap::from([
674 (9, SegmentRangeInclusive::new(0, 9)),
675 (19, SegmentRangeInclusive::new(10, 19)),
676 (24, SegmentRangeInclusive::new(20, 24))
677 ]))
678 )
679 }
680
681 {
682 let sf_rw: StaticFileProvider<EthPrimitives> =
683 StaticFileProviderBuilder::read_write(&static_dir)
684 .with_blocks_per_file(15)
685 .build()?;
686 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
687
688 let mut header = Header::default();
689 for num in 23..=40 {
690 header.number = num;
691 header_writer.append_header(&header, &BlockHash::default()).unwrap();
692 }
693 header_writer.commit().unwrap();
694
695 assert_eq!(sf_rw.headers_range(0..=40)?.len(), 41);
696 assert_eq!(
697 sf_rw.expected_block_index(StaticFileSegment::Headers),
698 Some(BTreeMap::from([
699 (9, SegmentRangeInclusive::new(0, 9)),
700 (19, SegmentRangeInclusive::new(10, 19)),
701 (24, SegmentRangeInclusive::new(20, 24)),
702 (39, SegmentRangeInclusive::new(25, 39)),
703 (54, SegmentRangeInclusive::new(40, 54))
704 ]))
705 )
706 }
707
708 Ok(())
709 }
710
711 #[test]
712 fn test_account_changeset_static_files() {
713 let (static_dir, _) = create_test_static_files_dir();
714
715 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
716 .expect("Failed to create static file provider");
717
718 fn generate_test_changesets(
720 block_num: u64,
721 addresses: Vec<Address>,
722 ) -> Vec<AccountBeforeTx> {
723 addresses
724 .into_iter()
725 .map(|address| AccountBeforeTx {
726 address,
727 info: Some(Account {
728 nonce: block_num,
729 balance: U256::from(block_num * 1000),
730 bytecode_hash: None,
731 }),
732 })
733 .collect()
734 }
735
736 {
738 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
739
740 let test_blocks = 10u64;
742 let addresses_per_block = 5;
743
744 for block_num in 0..test_blocks {
745 let addresses: Vec<Address> = (0..addresses_per_block)
747 .map(|i| {
748 let mut addr = Address::ZERO;
749 addr.0[0] = block_num as u8;
750 addr.0[1] = i as u8;
751 addr
752 })
753 .collect();
754
755 let changeset = generate_test_changesets(block_num, addresses.clone());
756
757 writer.append_account_changeset(changeset, block_num).unwrap();
758 }
759
760 writer.commit().unwrap();
761 }
762
763 {
765 let provider = sf_rw
766 .get_segment_provider_for_block(StaticFileSegment::AccountChangeSets, 5, None)
767 .unwrap();
768
769 let offsets = provider.read_changeset_offsets().unwrap();
771 assert!(offsets.is_some());
772 let offsets = offsets.unwrap();
773 assert_eq!(offsets.len(), 10); for (i, offset) in offsets.iter().enumerate() {
777 assert_eq!(offset.num_changes(), 5, "Block {} should have 5 changes", i);
778 }
779 }
780 }
781
782 #[test]
783 fn test_get_account_before_block() {
784 let (static_dir, _) = create_test_static_files_dir();
785
786 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
787 .expect("Failed to create static file provider");
788
789 let test_address = Address::from([1u8; 20]);
791 let other_address = Address::from([2u8; 20]);
792 let missing_address = Address::from([3u8; 20]);
793
794 {
796 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
797
798 writer
800 .append_account_changeset(
801 vec![
802 AccountBeforeTx {
803 address: test_address,
804 info: None, },
806 AccountBeforeTx { address: other_address, info: None },
807 ],
808 0,
809 )
810 .unwrap();
811
812 writer
814 .append_account_changeset(
815 vec![AccountBeforeTx {
816 address: other_address,
817 info: Some(Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }),
818 }],
819 1,
820 )
821 .unwrap();
822
823 writer
825 .append_account_changeset(
826 vec![AccountBeforeTx {
827 address: test_address,
828 info: Some(Account {
829 nonce: 1,
830 balance: U256::from(1000),
831 bytecode_hash: None,
832 }),
833 }],
834 2,
835 )
836 .unwrap();
837
838 writer.commit().unwrap();
839 }
840
841 {
843 let result = sf_rw.get_account_before_block(0, test_address).unwrap();
845 assert!(result.is_some());
846 let account_before = result.unwrap();
847 assert_eq!(account_before.address, test_address);
848 assert!(account_before.info.is_none()); let result = sf_rw.get_account_before_block(2, test_address).unwrap();
852 assert!(result.is_some());
853 let account_before = result.unwrap();
854 assert_eq!(account_before.address, test_address);
855 assert!(account_before.info.is_some());
856 let info = account_before.info.unwrap();
857 assert_eq!(info.nonce, 1);
858 assert_eq!(info.balance, U256::from(1000));
859
860 let result = sf_rw.get_account_before_block(1, test_address).unwrap();
862 assert!(result.is_none()); let result = sf_rw.get_account_before_block(2, missing_address).unwrap();
866 assert!(result.is_none());
867
868 let result = sf_rw.get_account_before_block(1, other_address).unwrap();
870 assert!(result.is_some());
871 let account_before = result.unwrap();
872 assert_eq!(account_before.address, other_address);
873 assert!(account_before.info.is_some());
874 }
875 }
876
877 #[test]
878 fn test_account_changeset_truncation() {
879 let (static_dir, _) = create_test_static_files_dir();
880
881 let blocks_per_file = 10;
882 let files_per_range = 4;
884 let file_set_count = 3;
885 let initial_file_count = files_per_range * file_set_count;
886 let tip = blocks_per_file * file_set_count - 1;
887
888 {
890 let sf_rw: StaticFileProvider<EthPrimitives> =
891 StaticFileProviderBuilder::read_write(&static_dir)
892 .with_blocks_per_file(blocks_per_file)
893 .build()
894 .expect("failed to create static file provider");
895
896 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
897
898 for block_num in 0..=tip {
899 let num_changes = ((block_num % 5) + 1) as usize;
901 let mut changeset = Vec::with_capacity(num_changes);
902
903 for i in 0..num_changes {
904 let mut address = Address::ZERO;
905 address.0[0] = block_num as u8;
906 address.0[1] = i as u8;
907
908 changeset.push(AccountBeforeTx {
909 address,
910 info: Some(Account {
911 nonce: block_num,
912 balance: U256::from(block_num * 1000 + i as u64),
913 bytecode_hash: None,
914 }),
915 });
916 }
917
918 writer.append_account_changeset(changeset, block_num).unwrap();
919 }
920
921 writer.commit().unwrap();
922 }
923
924 fn validate_truncation(
926 sf_rw: &StaticFileProvider<EthPrimitives>,
927 static_dir: impl AsRef<Path>,
928 expected_tip: Option<u64>,
929 expected_file_count: u64,
930 ) -> eyre::Result<()> {
931 let highest_block =
933 sf_rw.get_highest_static_file_block(StaticFileSegment::AccountChangeSets);
934 assert_eyre(highest_block, expected_tip, "block tip mismatch")?;
935
936 assert_eyre(
938 count_files_without_lockfile(static_dir)?,
939 expected_file_count as usize,
940 "file count mismatch",
941 )?;
942
943 if let Some(tip) = expected_tip {
944 let provider = sf_rw.get_segment_provider_for_block(
946 StaticFileSegment::AccountChangeSets,
947 tip,
948 None,
949 )?;
950
951 let offsets = provider.read_changeset_offsets().unwrap();
953 assert!(offsets.is_some(), "Should have changeset offsets");
954 }
955
956 Ok(())
957 }
958
959 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
961 .with_blocks_per_file(blocks_per_file)
962 .build()
963 .expect("failed to create static file provider");
964
965 sf_rw.initialize_index().expect("Failed to initialize index");
967
968 {
970 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
971 writer.prune_account_changesets(20).unwrap();
972 writer.commit().unwrap();
973
974 validate_truncation(&sf_rw, &static_dir, Some(20), initial_file_count)
975 .expect("Truncation validation failed");
976 }
977
978 {
980 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
981 writer.prune_account_changesets(9).unwrap();
982 writer.commit().unwrap();
983
984 validate_truncation(&sf_rw, &static_dir, Some(9), files_per_range)
985 .expect("Truncation validation failed");
986 }
987
988 {
990 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
991 writer.prune_account_changesets(0).unwrap();
992 writer.commit().unwrap();
993
994 validate_truncation(&sf_rw, &static_dir, Some(0), files_per_range)
996 .expect("Truncation validation failed");
997 }
998 }
999
1000 #[test]
1001 fn test_changeset_binary_search() {
1002 let (static_dir, _) = create_test_static_files_dir();
1003
1004 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1005 .expect("Failed to create static file provider");
1006
1007 let block_num = 0u64;
1009 let num_accounts = 100;
1010
1011 let mut addresses: Vec<Address> = Vec::with_capacity(num_accounts);
1012 for i in 0..num_accounts {
1013 let mut addr = Address::ZERO;
1014 addr.0[0] = (i / 256) as u8;
1015 addr.0[1] = (i % 256) as u8;
1016 addresses.push(addr);
1017 }
1018
1019 {
1021 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
1022
1023 let changeset: Vec<AccountBeforeTx> = addresses
1024 .iter()
1025 .map(|addr| AccountBeforeTx {
1026 address: *addr,
1027 info: Some(Account {
1028 nonce: 1,
1029 balance: U256::from(1000),
1030 bytecode_hash: None,
1031 }),
1032 })
1033 .collect();
1034
1035 writer.append_account_changeset(changeset, block_num).unwrap();
1036 writer.commit().unwrap();
1037 }
1038
1039 {
1041 let result = sf_rw.get_account_before_block(block_num, addresses[0]).unwrap();
1043 assert!(result.is_some());
1044 assert_eq!(result.unwrap().address, addresses[0]);
1045
1046 let result =
1048 sf_rw.get_account_before_block(block_num, addresses[num_accounts - 1]).unwrap();
1049 assert!(result.is_some());
1050 assert_eq!(result.unwrap().address, addresses[num_accounts - 1]);
1051
1052 let mid = num_accounts / 2;
1054 let result = sf_rw.get_account_before_block(block_num, addresses[mid]).unwrap();
1055 assert!(result.is_some());
1056 assert_eq!(result.unwrap().address, addresses[mid]);
1057
1058 let mut missing_addr = Address::ZERO;
1060 missing_addr.0[0] = 255;
1061 missing_addr.0[1] = 255;
1062 let result = sf_rw.get_account_before_block(block_num, missing_addr).unwrap();
1063 assert!(result.is_none());
1064
1065 for i in (0..num_accounts).step_by(10) {
1067 let result = sf_rw.get_account_before_block(block_num, addresses[i]).unwrap();
1068 assert!(result.is_some());
1069 assert_eq!(result.unwrap().address, addresses[i]);
1070 }
1071 }
1072 }
1073
1074 #[test]
1075 fn test_storage_changeset_static_files() {
1076 let (static_dir, _) = create_test_static_files_dir();
1077
1078 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1079 .expect("Failed to create static file provider");
1080
1081 {
1083 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1084
1085 let test_blocks = 10u64;
1087 let entries_per_block = 5;
1088
1089 for block_num in 0..test_blocks {
1090 let changeset = (0..entries_per_block)
1091 .map(|i| {
1092 let mut addr = Address::ZERO;
1093 addr.0[0] = block_num as u8;
1094 addr.0[1] = i as u8;
1095 StorageBeforeTx {
1096 address: addr,
1097 key: B256::with_last_byte(i as u8),
1098 value: U256::from(block_num * 1000 + i as u64),
1099 }
1100 })
1101 .collect::<Vec<_>>();
1102
1103 writer.append_storage_changeset(changeset, block_num).unwrap();
1104 }
1105
1106 writer.commit().unwrap();
1107 }
1108
1109 {
1111 let provider = sf_rw
1112 .get_segment_provider_for_block(StaticFileSegment::StorageChangeSets, 5, None)
1113 .unwrap();
1114
1115 let offsets = provider.read_changeset_offsets().unwrap();
1117 assert!(offsets.is_some());
1118 let offsets = offsets.unwrap();
1119 assert_eq!(offsets.len(), 10); for (i, offset) in offsets.iter().enumerate() {
1123 assert_eq!(offset.num_changes(), 5, "Block {} should have 5 changes", i);
1124 }
1125 }
1126 }
1127
1128 #[test]
1129 fn test_get_storage_before_block() {
1130 let (static_dir, _) = create_test_static_files_dir();
1131
1132 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1133 .expect("Failed to create static file provider");
1134
1135 let test_address = Address::from([1u8; 20]);
1136 let other_address = Address::from([2u8; 20]);
1137 let missing_address = Address::from([3u8; 20]);
1138 let test_key = B256::with_last_byte(1);
1139 let other_key = B256::with_last_byte(2);
1140
1141 {
1143 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1144
1145 writer
1147 .append_storage_changeset(
1148 vec![
1149 StorageBeforeTx { address: test_address, key: test_key, value: U256::ZERO },
1150 StorageBeforeTx {
1151 address: other_address,
1152 key: other_key,
1153 value: U256::from(5),
1154 },
1155 ],
1156 0,
1157 )
1158 .unwrap();
1159
1160 writer
1162 .append_storage_changeset(
1163 vec![StorageBeforeTx {
1164 address: other_address,
1165 key: other_key,
1166 value: U256::from(7),
1167 }],
1168 1,
1169 )
1170 .unwrap();
1171
1172 writer
1174 .append_storage_changeset(
1175 vec![StorageBeforeTx {
1176 address: test_address,
1177 key: test_key,
1178 value: U256::from(9),
1179 }],
1180 2,
1181 )
1182 .unwrap();
1183
1184 writer.commit().unwrap();
1185 }
1186
1187 {
1189 let result = sf_rw.get_storage_before_block(0, test_address, test_key).unwrap();
1190 assert!(result.is_some());
1191 let entry = result.unwrap();
1192 assert_eq!(entry.key, test_key);
1193 assert_eq!(entry.value, U256::ZERO);
1194
1195 let result = sf_rw.get_storage_before_block(2, test_address, test_key).unwrap();
1196 assert!(result.is_some());
1197 let entry = result.unwrap();
1198 assert_eq!(entry.key, test_key);
1199 assert_eq!(entry.value, U256::from(9));
1200
1201 let result = sf_rw.get_storage_before_block(1, test_address, test_key).unwrap();
1202 assert!(result.is_none());
1203
1204 let result = sf_rw.get_storage_before_block(2, missing_address, test_key).unwrap();
1205 assert!(result.is_none());
1206
1207 let result = sf_rw.get_storage_before_block(1, other_address, other_key).unwrap();
1208 assert!(result.is_some());
1209 let entry = result.unwrap();
1210 assert_eq!(entry.key, other_key);
1211 }
1212 }
1213
1214 #[test]
1215 fn test_storage_changeset_truncation() {
1216 let (static_dir, _) = create_test_static_files_dir();
1217
1218 let blocks_per_file = 10;
1219 let files_per_range = 4;
1221 let file_set_count = 3;
1222 let initial_file_count = files_per_range * file_set_count;
1223 let tip = blocks_per_file * file_set_count - 1;
1224
1225 {
1227 let sf_rw: StaticFileProvider<EthPrimitives> =
1228 StaticFileProviderBuilder::read_write(&static_dir)
1229 .with_blocks_per_file(blocks_per_file)
1230 .build()
1231 .expect("failed to create static file provider");
1232
1233 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1234
1235 for block_num in 0..=tip {
1236 let num_changes = ((block_num % 5) + 1) as usize;
1237 let mut changeset = Vec::with_capacity(num_changes);
1238
1239 for i in 0..num_changes {
1240 let mut address = Address::ZERO;
1241 address.0[0] = block_num as u8;
1242 address.0[1] = i as u8;
1243
1244 changeset.push(StorageBeforeTx {
1245 address,
1246 key: B256::with_last_byte(i as u8),
1247 value: U256::from(block_num * 1000 + i as u64),
1248 });
1249 }
1250
1251 writer.append_storage_changeset(changeset, block_num).unwrap();
1252 }
1253
1254 writer.commit().unwrap();
1255 }
1256
1257 fn validate_truncation(
1258 sf_rw: &StaticFileProvider<EthPrimitives>,
1259 static_dir: impl AsRef<Path>,
1260 expected_tip: Option<u64>,
1261 expected_file_count: u64,
1262 ) -> eyre::Result<()> {
1263 let highest_block =
1264 sf_rw.get_highest_static_file_block(StaticFileSegment::StorageChangeSets);
1265 assert_eyre(highest_block, expected_tip, "block tip mismatch")?;
1266
1267 assert_eyre(
1268 count_files_without_lockfile(static_dir)?,
1269 expected_file_count as usize,
1270 "file count mismatch",
1271 )?;
1272
1273 if let Some(tip) = expected_tip {
1274 let provider = sf_rw.get_segment_provider_for_block(
1275 StaticFileSegment::StorageChangeSets,
1276 tip,
1277 None,
1278 )?;
1279 let offsets = provider.read_changeset_offsets()?;
1280 assert!(offsets.is_some(), "Should have changeset offsets");
1281 }
1282
1283 Ok(())
1284 }
1285
1286 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
1287 .with_blocks_per_file(blocks_per_file)
1288 .build()
1289 .expect("failed to create static file provider");
1290
1291 sf_rw.initialize_index().expect("Failed to initialize index");
1292
1293 {
1295 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1296 writer.prune_storage_changesets(20).unwrap();
1297 writer.commit().unwrap();
1298
1299 validate_truncation(&sf_rw, &static_dir, Some(20), initial_file_count)
1300 .expect("Truncation validation failed");
1301 }
1302
1303 {
1305 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1306 writer.prune_storage_changesets(9).unwrap();
1307 writer.commit().unwrap();
1308
1309 validate_truncation(&sf_rw, &static_dir, Some(9), files_per_range)
1310 .expect("Truncation validation failed");
1311 }
1312
1313 {
1315 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1316 writer.prune_storage_changesets(0).unwrap();
1317 writer.commit().unwrap();
1318
1319 validate_truncation(&sf_rw, &static_dir, Some(0), files_per_range)
1320 .expect("Truncation validation failed");
1321 }
1322 }
1323
1324 #[test]
1325 fn test_storage_changeset_binary_search() {
1326 let (static_dir, _) = create_test_static_files_dir();
1327
1328 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1329 .expect("Failed to create static file provider");
1330
1331 let block_num = 0u64;
1332 let num_slots = 100;
1333 let address = Address::from([4u8; 20]);
1334
1335 let mut keys: Vec<B256> = Vec::with_capacity(num_slots);
1336 for i in 0..num_slots {
1337 keys.push(B256::with_last_byte(i as u8));
1338 }
1339
1340 {
1341 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1342 let changeset = keys
1343 .iter()
1344 .enumerate()
1345 .map(|(i, key)| StorageBeforeTx { address, key: *key, value: U256::from(i as u64) })
1346 .collect::<Vec<_>>();
1347
1348 writer.append_storage_changeset(changeset, block_num).unwrap();
1349 writer.commit().unwrap();
1350 }
1351
1352 {
1353 let result = sf_rw.get_storage_before_block(block_num, address, keys[0]).unwrap();
1354 assert!(result.is_some());
1355 let entry = result.unwrap();
1356 assert_eq!(entry.key, keys[0]);
1357 assert_eq!(entry.value, U256::from(0));
1358
1359 let result =
1360 sf_rw.get_storage_before_block(block_num, address, keys[num_slots - 1]).unwrap();
1361 assert!(result.is_some());
1362 let entry = result.unwrap();
1363 assert_eq!(entry.key, keys[num_slots - 1]);
1364
1365 let mid = num_slots / 2;
1366 let result = sf_rw.get_storage_before_block(block_num, address, keys[mid]).unwrap();
1367 assert!(result.is_some());
1368 let entry = result.unwrap();
1369 assert_eq!(entry.key, keys[mid]);
1370
1371 let missing_key = B256::with_last_byte(255);
1372 let result = sf_rw.get_storage_before_block(block_num, address, missing_key).unwrap();
1373 assert!(result.is_none());
1374
1375 for i in (0..num_slots).step_by(10) {
1376 let result = sf_rw.get_storage_before_block(block_num, address, keys[i]).unwrap();
1377 assert!(result.is_some());
1378 assert_eq!(result.unwrap().key, keys[i]);
1379 }
1380 }
1381 }
1382
1383 #[test]
1384 fn test_last_block_flushed_on_commit() {
1385 let (static_dir, _) = create_test_static_files_dir();
1386
1387 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1388 .expect("Failed to create static file provider");
1389
1390 let address = Address::from([5u8; 20]);
1391 let key = B256::with_last_byte(1);
1392
1393 {
1396 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1397
1398 writer
1400 .append_storage_changeset(
1401 vec![StorageBeforeTx { address, key, value: U256::from(42) }],
1402 0,
1403 )
1404 .unwrap();
1405
1406 writer.commit().unwrap();
1408 }
1409
1410 let highest = sf_rw.get_highest_static_file_block(StaticFileSegment::StorageChangeSets);
1412 assert_eq!(highest, Some(0), "Should have block 0 after commit");
1413
1414 let result = sf_rw.get_storage_before_block(0, address, key).unwrap();
1416 assert!(result.is_some(), "Should be able to read the changeset entry");
1417 let entry = result.unwrap();
1418 assert_eq!(entry.value, U256::from(42));
1419 }
1420}