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