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 assert_matches::assert_matches;
96 use rand::seq::SliceRandom;
97 use reth_db::{
98 models::{AccountBeforeTx, StorageBeforeTx},
99 test_utils::create_test_static_files_dir,
100 };
101 use reth_db_api::{transaction::DbTxMut, CanonicalHeaders, HeaderNumbers, Headers};
102 use reth_ethereum_primitives::{EthPrimitives, Receipt, TransactionSigned};
103 use reth_nippy_jar::NippyJarError;
104 use reth_primitives_traits::Account;
105 use reth_static_file_types::{
106 find_fixed_range, SegmentRangeInclusive, DEFAULT_BLOCKS_PER_STATIC_FILE,
107 };
108 use reth_storage_api::{
109 BlockHashReader, ChangeSetReader, ReceiptProvider, StorageChangeSetReader,
110 TransactionsProvider,
111 };
112 use reth_testing_utils::generators::{self, random_header_range};
113 use std::{collections::BTreeMap, fmt::Debug, fs, ops::Range, path::Path};
114
115 fn assert_eyre<T: PartialEq + Debug>(got: T, expected: T, msg: &str) -> eyre::Result<()> {
116 if got != expected {
117 eyre::bail!("{msg} | got: {got:?} expected: {expected:?}");
118 }
119 Ok(())
120 }
121
122 fn assert_cached_size_matches_disk(
123 sf_rw: &StaticFileProvider<EthPrimitives>,
124 segment: StaticFileSegment,
125 block: u64,
126 ) -> eyre::Result<()> {
127 let jar_provider = sf_rw.get_segment_provider_for_block(segment, block, None)?;
128 let disk_size = fs::metadata(jar_provider.data_path())?.len() +
129 fs::metadata(jar_provider.offsets_path())?.len();
130 assert_eyre(jar_provider.size() as u64, disk_size, "cached size mismatch")
131 }
132
133 #[test]
134 fn test_static_files() {
135 let row_count = 100u64;
137 let range = 0..=(row_count - 1);
138
139 let factory = create_test_provider_factory();
141 let static_files_path = tempfile::tempdir().unwrap();
142 let static_file = static_files_path.path().join(
143 StaticFileSegment::Headers
144 .filename(&find_fixed_range(*range.end(), DEFAULT_BLOCKS_PER_STATIC_FILE)),
145 );
146
147 let mut headers = random_header_range(
149 &mut generators::rng(),
150 *range.start()..(*range.end() + 1),
151 B256::random(),
152 );
153
154 let mut provider_rw = factory.provider_rw().unwrap();
155 let tx = provider_rw.tx_mut();
156 for header in headers.clone() {
157 let hash = header.hash();
158
159 tx.put::<CanonicalHeaders>(header.number, hash).unwrap();
160 tx.put::<Headers>(header.number, header.clone_header()).unwrap();
161 tx.put::<HeaderNumbers>(hash, header.number).unwrap();
162 }
163 provider_rw.commit().unwrap();
164
165 {
167 let manager = factory.static_file_provider();
168 let mut writer = manager.latest_writer(StaticFileSegment::Headers).unwrap();
169
170 for header in headers.clone() {
171 let hash = header.hash();
172 writer.append_header(&header.unseal(), &hash).unwrap();
173 }
174 writer.commit().unwrap();
175 }
176
177 {
179 let db_provider = factory.provider().unwrap();
180 let manager = db_provider.static_file_provider();
181 let jar_provider = manager
182 .get_segment_provider_for_block(StaticFileSegment::Headers, 0, Some(&static_file))
183 .unwrap();
184
185 assert!(!headers.is_empty());
186
187 headers.shuffle(&mut generators::rng());
189
190 for header in headers {
191 let header_hash = header.hash();
192 let header = header.unseal();
193
194 assert_eq!(header, db_provider.header(header_hash).unwrap().unwrap());
196 assert_eq!(header, jar_provider.header_by_number(header.number).unwrap().unwrap());
197 }
198 }
199 }
200
201 #[test]
202 fn test_header_truncation() {
203 let (static_dir, _) = create_test_static_files_dir();
204
205 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;
209 let tip = blocks_per_file * file_set_count - 1; {
213 let sf_rw: StaticFileProvider<EthPrimitives> =
214 StaticFileProviderBuilder::read_write(&static_dir)
215 .with_blocks_per_file(blocks_per_file)
216 .build()
217 .expect("Failed to build static file provider");
218
219 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
220
221 let mut header = Header::default();
223 for num in 0..=tip {
224 header.number = num;
225 header_writer.append_header(&header, &BlockHash::default()).unwrap();
226 }
227 header_writer.commit().unwrap();
228 }
229
230 fn prune_and_validate(
232 writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
233 sf_rw: &StaticFileProvider<EthPrimitives>,
234 static_dir: impl AsRef<Path>,
235 prune_count: u64,
236 expected_tip: Option<u64>,
237 expected_file_count: u64,
238 ) -> eyre::Result<()> {
239 writer.prune_headers(prune_count)?;
240 writer.commit()?;
241
242 assert_eyre(
244 sf_rw.get_highest_static_file_block(StaticFileSegment::Headers),
245 expected_tip,
246 "block mismatch",
247 )?;
248
249 if let Some(id) = expected_tip {
250 assert_eyre(
251 sf_rw.header_by_number(id)?.map(|h| h.number),
252 expected_tip,
253 "header mismatch",
254 )?;
255 assert_cached_size_matches_disk(sf_rw, StaticFileSegment::Headers, id)?;
256 }
257
258 assert_eyre(
260 count_files_without_lockfile(static_dir)?,
261 expected_file_count as usize,
262 "file count mismatch",
263 )?;
264
265 Ok(())
266 }
267
268 type PruneCount = u64;
270 type ExpectedTip = u64;
271 type ExpectedFileCount = u64;
272 let mut tmp_tip = tip;
273 let test_cases: Vec<(PruneCount, Option<ExpectedTip>, ExpectedFileCount)> = vec![
274 {
276 tmp_tip -= 1;
277 (1, Some(tmp_tip), initial_file_count)
278 },
279 {
281 tmp_tip -= blocks_per_file - 1;
282 (blocks_per_file - 1, Some(tmp_tip), initial_file_count - files_per_range)
283 },
284 {
287 tmp_tip -= blocks_per_file + 1;
288 (blocks_per_file + 1, Some(tmp_tip), initial_file_count - files_per_range * 2)
289 },
290 {
292 (
293 tmp_tip,
294 Some(0), files_per_range, )
297 },
298 {
300 (
301 1,
302 None, files_per_range, )
305 },
306 ];
307
308 {
310 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
311 .with_blocks_per_file(blocks_per_file)
312 .build()
313 .expect("Failed to build static file provider");
314
315 assert_eq!(sf_rw.get_highest_static_file_block(StaticFileSegment::Headers), Some(tip));
316 assert_eq!(
317 count_files_without_lockfile(static_dir.as_ref()).unwrap(),
318 initial_file_count as usize
319 );
320
321 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
322
323 for (case, (prune_count, expected_tip, expected_file_count)) in
324 test_cases.into_iter().enumerate()
325 {
326 prune_and_validate(
327 &mut header_writer,
328 &sf_rw,
329 &static_dir,
330 prune_count,
331 expected_tip,
332 expected_file_count,
333 )
334 .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
335 .unwrap();
336 }
337 }
338 }
339
340 fn setup_tx_based_scenario(
347 sf_rw: &StaticFileProvider<EthPrimitives>,
348 segment: StaticFileSegment,
349 blocks_per_file: u64,
350 ) {
351 fn setup_block_ranges(
352 writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
353 sf_rw: &StaticFileProvider<EthPrimitives>,
354 segment: StaticFileSegment,
355 block_range: &Range<u64>,
356 mut tx_count: u64,
357 next_tx_num: &mut u64,
358 ) {
359 let mut receipt = Receipt::default();
360 let mut tx = TxLegacy::default();
361
362 for block in block_range.clone() {
363 writer.increment_block(block).unwrap();
364
365 if tx_count > 0 {
367 match segment {
368 StaticFileSegment::Headers |
369 StaticFileSegment::AccountChangeSets |
370 StaticFileSegment::StorageChangeSets => {
371 panic!("non tx based segment")
372 }
373 StaticFileSegment::Transactions => {
374 tx.nonce = *next_tx_num;
376 let tx: TransactionSigned =
377 tx.clone().into_signed(Signature::test_signature()).into();
378 writer.append_transaction(*next_tx_num, &tx).unwrap();
379 }
380 StaticFileSegment::Receipts => {
381 receipt.cumulative_gas_used = *next_tx_num;
383 writer.append_receipt(*next_tx_num, &receipt).unwrap();
384 }
385 StaticFileSegment::TransactionSenders => {
386 let sender = Address::from(U160::from(*next_tx_num));
388 writer.append_transaction_sender(*next_tx_num, &sender).unwrap();
389 }
390 }
391 *next_tx_num += 1;
392 tx_count -= 1;
393 }
394 }
395 writer.commit().unwrap();
396
397 let expected_block = block_range.end - 1;
399 let expected_tx = if tx_count == 0 { *next_tx_num - 1 } else { *next_tx_num };
400
401 assert_eq!(sf_rw.get_highest_static_file_block(segment), Some(expected_block),);
403 assert_eq!(sf_rw.get_highest_static_file_tx(segment), Some(expected_tx),);
404 }
405
406 let block_ranges = [
408 0..blocks_per_file,
409 blocks_per_file..blocks_per_file * 2,
410 blocks_per_file * 2..blocks_per_file * 3,
411 ];
412
413 let tx_counts = [
414 blocks_per_file - 1, 0, 1, ];
418
419 let mut writer = sf_rw.latest_writer(segment).unwrap();
420 let mut next_tx_num = 0;
421
422 for (block_range, tx_count) in block_ranges.iter().zip(tx_counts.iter()) {
424 setup_block_ranges(
425 &mut writer,
426 sf_rw,
427 segment,
428 block_range,
429 *tx_count,
430 &mut next_tx_num,
431 );
432 }
433
434 let expected_tx_ranges = vec![
436 Some(SegmentRangeInclusive::new(0, 8)),
437 None,
438 Some(SegmentRangeInclusive::new(9, 9)),
439 ];
440
441 block_ranges.iter().zip(expected_tx_ranges).for_each(|(block_range, expected_tx_range)| {
442 assert_eq!(
443 sf_rw
444 .get_segment_provider_for_block(segment, block_range.start, None)
445 .unwrap()
446 .user_header()
447 .tx_range(),
448 expected_tx_range
449 );
450 });
451
452 let expected_tx_index = BTreeMap::from([
454 (8, SegmentRangeInclusive::new(0, 9)),
455 (9, SegmentRangeInclusive::new(20, 29)),
456 ]);
457 assert_eq!(
458 sf_rw.tx_index(segment),
459 (!expected_tx_index.is_empty()).then_some(expected_tx_index),
460 "tx index mismatch",
461 );
462 }
463
464 #[test]
465 fn test_tx_based_truncation() {
466 let segments = [StaticFileSegment::Transactions, StaticFileSegment::Receipts];
467 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;
471
472 #[expect(clippy::too_many_arguments)]
473 fn prune_and_validate(
474 sf_rw: &StaticFileProvider<EthPrimitives>,
475 static_dir: impl AsRef<Path>,
476 segment: StaticFileSegment,
477 prune_count: u64,
478 last_block: u64,
479 expected_tx_tip: Option<u64>,
480 expected_file_count: i32,
481 expected_tx_index: BTreeMap<TxNumber, SegmentRangeInclusive>,
482 ) -> eyre::Result<()> {
483 let mut writer = sf_rw.latest_writer(segment)?;
484
485 match segment {
487 StaticFileSegment::Headers |
488 StaticFileSegment::AccountChangeSets |
489 StaticFileSegment::StorageChangeSets => {
490 panic!("non tx based segment")
491 }
492 StaticFileSegment::Transactions => {
493 writer.prune_transactions(prune_count, last_block)?
494 }
495 StaticFileSegment::Receipts => writer.prune_receipts(prune_count, last_block)?,
496 StaticFileSegment::TransactionSenders => {
497 writer.prune_transaction_senders(prune_count, last_block)?
498 }
499 }
500 writer.commit()?;
501
502 assert_eyre(
504 sf_rw.get_highest_static_file_block(segment),
505 Some(last_block),
506 "block mismatch",
507 )?;
508 assert_eyre(sf_rw.get_highest_static_file_tx(segment), expected_tx_tip, "tx mismatch")?;
509 assert_cached_size_matches_disk(sf_rw, segment, last_block)?;
510
511 if let Some(id) = expected_tx_tip {
514 match segment {
515 StaticFileSegment::Headers |
516 StaticFileSegment::AccountChangeSets |
517 StaticFileSegment::StorageChangeSets => {
518 panic!("non tx based segment")
519 }
520 StaticFileSegment::Transactions => assert_eyre(
521 expected_tx_tip,
522 sf_rw.transaction_by_id(id)?.map(|t| t.nonce()),
523 "tx mismatch",
524 )?,
525 StaticFileSegment::Receipts => assert_eyre(
526 expected_tx_tip,
527 sf_rw.receipt(id)?.map(|r| r.cumulative_gas_used),
528 "receipt mismatch",
529 )?,
530 StaticFileSegment::TransactionSenders => assert_eyre(
531 expected_tx_tip,
532 sf_rw
533 .transaction_sender(id)?
534 .map(|s| u64::try_from(U160::from_be_bytes(s.0.into())).unwrap()),
535 "sender mismatch",
536 )?,
537 }
538 }
539
540 assert_eyre(
542 count_files_without_lockfile(static_dir)?,
543 expected_file_count as usize,
544 "file count mismatch",
545 )?;
546
547 assert_eyre(
549 sf_rw.tx_index(segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()),
550 (!expected_tx_index.is_empty()).then_some(expected_tx_index),
551 "tx index mismatch",
552 )?;
553
554 Ok(())
555 }
556
557 for segment in segments {
558 let (static_dir, _) = create_test_static_files_dir();
559
560 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
561 .with_blocks_per_file(blocks_per_file)
562 .build()
563 .expect("Failed to build static file provider");
564
565 setup_tx_based_scenario(&sf_rw, segment, blocks_per_file);
566
567 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
568 .with_blocks_per_file(blocks_per_file)
569 .build()
570 .expect("Failed to build static file provider");
571 let highest_tx = sf_rw.get_highest_static_file_tx(segment).unwrap();
572
573 let test_cases = vec![
576 (
581 1,
582 blocks_per_file * 2,
583 Some(highest_tx - 1),
584 initial_file_count,
585 BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
586 ),
587 (
591 0,
592 blocks_per_file - 1,
593 Some(highest_tx - 1),
594 files_per_range,
595 BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
596 ),
597 (
599 highest_tx - 1,
600 1,
601 Some(0),
602 files_per_range,
603 BTreeMap::from([(0, SegmentRangeInclusive::new(0, 1))]),
604 ),
605 (1, 0, None, files_per_range, BTreeMap::from([])),
607 ];
608
609 for (
611 case,
612 (prune_count, last_block, expected_tx_tip, expected_file_count, expected_tx_index),
613 ) in test_cases.into_iter().enumerate()
614 {
615 prune_and_validate(
616 &sf_rw,
617 &static_dir,
618 segment,
619 prune_count,
620 last_block,
621 expected_tx_tip,
622 expected_file_count,
623 expected_tx_index,
624 )
625 .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
626 .unwrap();
627 }
628 }
629 }
630
631 fn count_files_without_lockfile(path: impl AsRef<Path>) -> eyre::Result<usize> {
633 let is_lockfile =
634 |entry: &fs::DirEntry| entry.path().file_name().is_some_and(|name| name == "lock");
635 let count = fs::read_dir(path)?
636 .filter_map(|entry| entry.ok())
637 .filter(|entry| !is_lockfile(entry))
638 .count();
639
640 Ok(count)
641 }
642
643 #[test]
644 fn test_dynamic_size() -> eyre::Result<()> {
645 let (static_dir, _) = create_test_static_files_dir();
646
647 {
648 let sf_rw: StaticFileProvider<EthPrimitives> =
649 StaticFileProviderBuilder::read_write(&static_dir)
650 .with_blocks_per_file(10)
651 .build()?;
652 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
653
654 let mut header = Header::default();
655 for num in 0..=15 {
656 header.number = num;
657 header_writer.append_header(&header, &BlockHash::default()).unwrap();
658 }
659 header_writer.commit().unwrap();
660
661 assert_eq!(sf_rw.headers_range(0..=15)?.len(), 16);
662 assert_eq!(
663 sf_rw.expected_block_index(StaticFileSegment::Headers),
664 Some(BTreeMap::from([
665 (9, SegmentRangeInclusive::new(0, 9)),
666 (19, SegmentRangeInclusive::new(10, 19))
667 ])),
668 )
669 }
670
671 {
672 let sf_rw: StaticFileProvider<EthPrimitives> =
673 StaticFileProviderBuilder::read_write(&static_dir)
674 .with_blocks_per_file(5)
675 .build()?;
676 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
677
678 let mut header = Header::default();
679 for num in 16..=22 {
680 header.number = num;
681 header_writer.append_header(&header, &BlockHash::default()).unwrap();
682 }
683 header_writer.commit().unwrap();
684
685 assert_eq!(sf_rw.headers_range(0..=22)?.len(), 23);
686 assert_eq!(
687 sf_rw.expected_block_index(StaticFileSegment::Headers),
688 Some(BTreeMap::from([
689 (9, SegmentRangeInclusive::new(0, 9)),
690 (19, SegmentRangeInclusive::new(10, 19)),
691 (24, SegmentRangeInclusive::new(20, 24))
692 ]))
693 )
694 }
695
696 {
697 let sf_rw: StaticFileProvider<EthPrimitives> =
698 StaticFileProviderBuilder::read_write(&static_dir)
699 .with_blocks_per_file(15)
700 .build()?;
701 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
702
703 let mut header = Header::default();
704 for num in 23..=40 {
705 header.number = num;
706 header_writer.append_header(&header, &BlockHash::default()).unwrap();
707 }
708 header_writer.commit().unwrap();
709
710 assert_eq!(sf_rw.headers_range(0..=40)?.len(), 41);
711 assert_eq!(
712 sf_rw.expected_block_index(StaticFileSegment::Headers),
713 Some(BTreeMap::from([
714 (9, SegmentRangeInclusive::new(0, 9)),
715 (19, SegmentRangeInclusive::new(10, 19)),
716 (24, SegmentRangeInclusive::new(20, 24)),
717 (39, SegmentRangeInclusive::new(25, 39)),
718 (54, SegmentRangeInclusive::new(40, 54))
719 ]))
720 )
721 }
722
723 Ok(())
724 }
725
726 #[test]
727 fn test_account_changeset_static_files() {
728 let (static_dir, _) = create_test_static_files_dir();
729
730 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
731 .expect("Failed to create static file provider");
732
733 fn generate_test_changesets(
735 block_num: u64,
736 addresses: Vec<Address>,
737 ) -> Vec<AccountBeforeTx> {
738 addresses
739 .into_iter()
740 .map(|address| AccountBeforeTx {
741 address,
742 info: Some(Account {
743 nonce: block_num,
744 balance: U256::from(block_num * 1000),
745 ..Default::default()
746 }),
747 })
748 .collect()
749 }
750
751 {
753 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
754
755 let test_blocks = 10u64;
757 let addresses_per_block = 5;
758
759 for block_num in 0..test_blocks {
760 let addresses: Vec<Address> = (0..addresses_per_block)
762 .map(|i| {
763 let mut addr = Address::ZERO;
764 addr.0[0] = block_num as u8;
765 addr.0[1] = i as u8;
766 addr
767 })
768 .collect();
769
770 let changeset = generate_test_changesets(block_num, addresses);
771
772 writer.append_account_changeset(changeset, block_num).unwrap();
773 }
774
775 writer.commit().unwrap();
776 }
777
778 {
780 let provider = sf_rw
781 .get_segment_provider_for_block(StaticFileSegment::AccountChangeSets, 5, None)
782 .unwrap();
783
784 let offsets = provider.read_changeset_offsets().unwrap();
786 assert!(offsets.is_some());
787 let offsets = offsets.unwrap();
788 assert_eq!(offsets.len(), 10); for (i, offset) in offsets.iter().enumerate() {
792 assert_eq!(offset.num_changes(), 5, "Block {} should have 5 changes", i);
793 }
794 }
795 }
796
797 #[test]
798 fn test_get_account_before_block() {
799 let (static_dir, _) = create_test_static_files_dir();
800
801 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
802 .expect("Failed to create static file provider");
803
804 let test_address = Address::repeat_byte(1u8);
806 let other_address = Address::repeat_byte(2u8);
807 let missing_address = Address::repeat_byte(3u8);
808
809 {
811 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
812
813 writer
815 .append_account_changeset(
816 vec![
817 AccountBeforeTx {
818 address: test_address,
819 info: None, },
821 AccountBeforeTx { address: other_address, info: None },
822 ],
823 0,
824 )
825 .unwrap();
826
827 writer
829 .append_account_changeset(
830 vec![AccountBeforeTx {
831 address: other_address,
832 info: Some(Account::default()),
833 }],
834 1,
835 )
836 .unwrap();
837
838 writer
840 .append_account_changeset(
841 vec![AccountBeforeTx {
842 address: test_address,
843 info: Some(Account {
844 nonce: 1,
845 balance: U256::from(1000),
846 ..Default::default()
847 }),
848 }],
849 2,
850 )
851 .unwrap();
852
853 writer.commit().unwrap();
854 }
855
856 {
858 let result = sf_rw.get_account_before_block(0, test_address).unwrap();
860 assert!(result.is_some());
861 let account_before = result.unwrap();
862 assert_eq!(account_before.address, test_address);
863 assert!(account_before.info.is_none()); let result = sf_rw.get_account_before_block(2, test_address).unwrap();
867 assert!(result.is_some());
868 let account_before = result.unwrap();
869 assert_eq!(account_before.address, test_address);
870 assert!(account_before.info.is_some());
871 let info = account_before.info.unwrap();
872 assert_eq!(info.nonce, 1);
873 assert_eq!(info.balance, U256::from(1000));
874
875 let result = sf_rw.get_account_before_block(1, test_address).unwrap();
877 assert!(result.is_none()); let result = sf_rw.get_account_before_block(2, missing_address).unwrap();
881 assert!(result.is_none());
882
883 let result = sf_rw.get_account_before_block(1, other_address).unwrap();
885 assert!(result.is_some());
886 let account_before = result.unwrap();
887 assert_eq!(account_before.address, other_address);
888 assert!(account_before.info.is_some());
889 }
890 }
891
892 #[test]
893 fn test_account_changeset_truncation() {
894 let (static_dir, _) = create_test_static_files_dir();
895
896 let blocks_per_file = 10;
897 let files_per_range = 4;
899 let file_set_count = 3;
900 let initial_file_count = files_per_range * file_set_count;
901 let tip = blocks_per_file * file_set_count - 1;
902
903 {
905 let sf_rw: StaticFileProvider<EthPrimitives> =
906 StaticFileProviderBuilder::read_write(&static_dir)
907 .with_blocks_per_file(blocks_per_file)
908 .build()
909 .expect("failed to create static file provider");
910
911 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
912
913 for block_num in 0..=tip {
914 let num_changes = ((block_num % 5) + 1) as usize;
916 let mut changeset = Vec::with_capacity(num_changes);
917
918 for i in 0..num_changes {
919 let mut address = Address::ZERO;
920 address.0[0] = block_num as u8;
921 address.0[1] = i as u8;
922
923 changeset.push(AccountBeforeTx {
924 address,
925 info: Some(Account {
926 nonce: block_num,
927 balance: U256::from(block_num * 1000 + i as u64),
928 ..Default::default()
929 }),
930 });
931 }
932
933 writer.append_account_changeset(changeset, block_num).unwrap();
934 }
935
936 writer.commit().unwrap();
937 }
938
939 fn validate_truncation(
941 sf_rw: &StaticFileProvider<EthPrimitives>,
942 static_dir: impl AsRef<Path>,
943 expected_tip: Option<u64>,
944 expected_file_count: u64,
945 ) -> eyre::Result<()> {
946 let highest_block =
948 sf_rw.get_highest_static_file_block(StaticFileSegment::AccountChangeSets);
949 assert_eyre(highest_block, expected_tip, "block tip mismatch")?;
950
951 assert_eyre(
953 count_files_without_lockfile(static_dir)?,
954 expected_file_count as usize,
955 "file count mismatch",
956 )?;
957
958 if let Some(tip) = expected_tip {
959 let provider = sf_rw.get_segment_provider_for_block(
961 StaticFileSegment::AccountChangeSets,
962 tip,
963 None,
964 )?;
965
966 let offsets = provider.read_changeset_offsets().unwrap();
968 assert!(offsets.is_some(), "Should have changeset offsets");
969 }
970
971 Ok(())
972 }
973
974 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
976 .with_blocks_per_file(blocks_per_file)
977 .build()
978 .expect("failed to create static file provider");
979
980 sf_rw.initialize_index().expect("Failed to initialize index");
982
983 {
985 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
986 writer.prune_account_changesets(20).unwrap();
987 writer.commit().unwrap();
988
989 validate_truncation(&sf_rw, &static_dir, Some(20), initial_file_count)
990 .expect("Truncation validation failed");
991 }
992
993 {
995 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
996 writer.prune_account_changesets(9).unwrap();
997 writer.commit().unwrap();
998
999 validate_truncation(&sf_rw, &static_dir, Some(9), files_per_range)
1000 .expect("Truncation validation failed");
1001 }
1002
1003 {
1005 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
1006 writer.prune_account_changesets(0).unwrap();
1007 writer.commit().unwrap();
1008
1009 validate_truncation(&sf_rw, &static_dir, Some(0), files_per_range)
1011 .expect("Truncation validation failed");
1012 }
1013 }
1014
1015 #[test]
1016 fn test_changeset_binary_search() {
1017 let (static_dir, _) = create_test_static_files_dir();
1018
1019 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1020 .expect("Failed to create static file provider");
1021
1022 let block_num = 0u64;
1024 let num_accounts = 100;
1025
1026 let mut addresses: Vec<Address> = Vec::with_capacity(num_accounts);
1027 for i in 0..num_accounts {
1028 let mut addr = Address::ZERO;
1029 addr.0[0] = (i / 256) as u8;
1030 addr.0[1] = (i % 256) as u8;
1031 addresses.push(addr);
1032 }
1033
1034 {
1036 let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
1037
1038 let changeset: Vec<AccountBeforeTx> = addresses
1039 .iter()
1040 .map(|addr| AccountBeforeTx {
1041 address: *addr,
1042 info: Some(Account {
1043 nonce: 1,
1044 balance: U256::from(1000),
1045 ..Default::default()
1046 }),
1047 })
1048 .collect();
1049
1050 writer.append_account_changeset(changeset, block_num).unwrap();
1051 writer.commit().unwrap();
1052 }
1053
1054 {
1056 let result = sf_rw.get_account_before_block(block_num, addresses[0]).unwrap();
1058 assert!(result.is_some());
1059 assert_eq!(result.unwrap().address, addresses[0]);
1060
1061 let result =
1063 sf_rw.get_account_before_block(block_num, addresses[num_accounts - 1]).unwrap();
1064 assert!(result.is_some());
1065 assert_eq!(result.unwrap().address, addresses[num_accounts - 1]);
1066
1067 let mid = num_accounts / 2;
1069 let result = sf_rw.get_account_before_block(block_num, addresses[mid]).unwrap();
1070 assert!(result.is_some());
1071 assert_eq!(result.unwrap().address, addresses[mid]);
1072
1073 let mut missing_addr = Address::ZERO;
1075 missing_addr.0[0] = 255;
1076 missing_addr.0[1] = 255;
1077 let result = sf_rw.get_account_before_block(block_num, missing_addr).unwrap();
1078 assert!(result.is_none());
1079
1080 for i in (0..num_accounts).step_by(10) {
1082 let result = sf_rw.get_account_before_block(block_num, addresses[i]).unwrap();
1083 assert!(result.is_some());
1084 assert_eq!(result.unwrap().address, addresses[i]);
1085 }
1086 }
1087 }
1088
1089 #[test]
1090 fn test_storage_changeset_static_files() {
1091 let (static_dir, _) = create_test_static_files_dir();
1092
1093 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1094 .expect("Failed to create static file provider");
1095
1096 {
1098 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1099
1100 let test_blocks = 10u64;
1102 let entries_per_block = 5;
1103
1104 for block_num in 0..test_blocks {
1105 let changeset = (0..entries_per_block)
1106 .map(|i| {
1107 let mut addr = Address::ZERO;
1108 addr.0[0] = block_num as u8;
1109 addr.0[1] = i as u8;
1110 StorageBeforeTx {
1111 address: addr,
1112 key: B256::with_last_byte(i as u8),
1113 value: U256::from(block_num * 1000 + i as u64),
1114 }
1115 })
1116 .collect::<Vec<_>>();
1117
1118 writer.append_storage_changeset(changeset, block_num).unwrap();
1119 }
1120
1121 writer.commit().unwrap();
1122 }
1123
1124 {
1126 let provider = sf_rw
1127 .get_segment_provider_for_block(StaticFileSegment::StorageChangeSets, 5, None)
1128 .unwrap();
1129
1130 let offsets = provider.read_changeset_offsets().unwrap();
1132 assert!(offsets.is_some());
1133 let offsets = offsets.unwrap();
1134 assert_eq!(offsets.len(), 10); for (i, offset) in offsets.iter().enumerate() {
1138 assert_eq!(offset.num_changes(), 5, "Block {} should have 5 changes", i);
1139 }
1140 }
1141 }
1142
1143 #[test]
1144 fn test_get_storage_before_block() {
1145 let (static_dir, _) = create_test_static_files_dir();
1146
1147 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1148 .expect("Failed to create static file provider");
1149
1150 let test_address = Address::repeat_byte(1u8);
1151 let other_address = Address::repeat_byte(2u8);
1152 let missing_address = Address::repeat_byte(3u8);
1153 let test_key = B256::with_last_byte(1);
1154 let other_key = B256::with_last_byte(2);
1155
1156 {
1158 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1159
1160 writer
1162 .append_storage_changeset(
1163 vec![
1164 StorageBeforeTx { address: test_address, key: test_key, value: U256::ZERO },
1165 StorageBeforeTx {
1166 address: other_address,
1167 key: other_key,
1168 value: U256::from(5),
1169 },
1170 ],
1171 0,
1172 )
1173 .unwrap();
1174
1175 writer
1177 .append_storage_changeset(
1178 vec![StorageBeforeTx {
1179 address: other_address,
1180 key: other_key,
1181 value: U256::from(7),
1182 }],
1183 1,
1184 )
1185 .unwrap();
1186
1187 writer
1189 .append_storage_changeset(
1190 vec![StorageBeforeTx {
1191 address: test_address,
1192 key: test_key,
1193 value: U256::from(9),
1194 }],
1195 2,
1196 )
1197 .unwrap();
1198
1199 writer.commit().unwrap();
1200 }
1201
1202 {
1204 let result = sf_rw.get_storage_before_block(0, test_address, test_key).unwrap();
1205 assert!(result.is_some());
1206 let entry = result.unwrap();
1207 assert_eq!(entry.key, test_key);
1208 assert_eq!(entry.value, U256::ZERO);
1209
1210 let result = sf_rw.get_storage_before_block(2, test_address, test_key).unwrap();
1211 assert!(result.is_some());
1212 let entry = result.unwrap();
1213 assert_eq!(entry.key, test_key);
1214 assert_eq!(entry.value, U256::from(9));
1215
1216 let result = sf_rw.get_storage_before_block(1, test_address, test_key).unwrap();
1217 assert!(result.is_none());
1218
1219 let result = sf_rw.get_storage_before_block(2, missing_address, test_key).unwrap();
1220 assert!(result.is_none());
1221
1222 let result = sf_rw.get_storage_before_block(1, other_address, other_key).unwrap();
1223 assert!(result.is_some());
1224 let entry = result.unwrap();
1225 assert_eq!(entry.key, other_key);
1226 }
1227 }
1228
1229 #[test]
1230 fn test_storage_changeset_truncation() {
1231 let (static_dir, _) = create_test_static_files_dir();
1232
1233 let blocks_per_file = 10;
1234 let files_per_range = 4;
1236 let file_set_count = 3;
1237 let initial_file_count = files_per_range * file_set_count;
1238 let tip = blocks_per_file * file_set_count - 1;
1239
1240 {
1242 let sf_rw: StaticFileProvider<EthPrimitives> =
1243 StaticFileProviderBuilder::read_write(&static_dir)
1244 .with_blocks_per_file(blocks_per_file)
1245 .build()
1246 .expect("failed to create static file provider");
1247
1248 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1249
1250 for block_num in 0..=tip {
1251 let num_changes = ((block_num % 5) + 1) as usize;
1252 let mut changeset = Vec::with_capacity(num_changes);
1253
1254 for i in 0..num_changes {
1255 let mut address = Address::ZERO;
1256 address.0[0] = block_num as u8;
1257 address.0[1] = i as u8;
1258
1259 changeset.push(StorageBeforeTx {
1260 address,
1261 key: B256::with_last_byte(i as u8),
1262 value: U256::from(block_num * 1000 + i as u64),
1263 });
1264 }
1265
1266 writer.append_storage_changeset(changeset, block_num).unwrap();
1267 }
1268
1269 writer.commit().unwrap();
1270 }
1271
1272 fn validate_truncation(
1273 sf_rw: &StaticFileProvider<EthPrimitives>,
1274 static_dir: impl AsRef<Path>,
1275 expected_tip: Option<u64>,
1276 expected_file_count: u64,
1277 ) -> eyre::Result<()> {
1278 let highest_block =
1279 sf_rw.get_highest_static_file_block(StaticFileSegment::StorageChangeSets);
1280 assert_eyre(highest_block, expected_tip, "block tip mismatch")?;
1281
1282 assert_eyre(
1283 count_files_without_lockfile(static_dir)?,
1284 expected_file_count as usize,
1285 "file count mismatch",
1286 )?;
1287
1288 if let Some(tip) = expected_tip {
1289 let provider = sf_rw.get_segment_provider_for_block(
1290 StaticFileSegment::StorageChangeSets,
1291 tip,
1292 None,
1293 )?;
1294 let offsets = provider.read_changeset_offsets()?;
1295 assert!(offsets.is_some(), "Should have changeset offsets");
1296 }
1297
1298 Ok(())
1299 }
1300
1301 let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
1302 .with_blocks_per_file(blocks_per_file)
1303 .build()
1304 .expect("failed to create static file provider");
1305
1306 sf_rw.initialize_index().expect("Failed to initialize index");
1307
1308 {
1310 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1311 writer.prune_storage_changesets(20).unwrap();
1312 writer.commit().unwrap();
1313
1314 validate_truncation(&sf_rw, &static_dir, Some(20), initial_file_count)
1315 .expect("Truncation validation failed");
1316 }
1317
1318 {
1320 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1321 writer.prune_storage_changesets(9).unwrap();
1322 writer.commit().unwrap();
1323
1324 validate_truncation(&sf_rw, &static_dir, Some(9), files_per_range)
1325 .expect("Truncation validation failed");
1326 }
1327
1328 {
1330 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1331 writer.prune_storage_changesets(0).unwrap();
1332 writer.commit().unwrap();
1333
1334 validate_truncation(&sf_rw, &static_dir, Some(0), files_per_range)
1335 .expect("Truncation validation failed");
1336 }
1337 }
1338
1339 #[test]
1340 fn test_storage_changeset_binary_search() {
1341 let (static_dir, _) = create_test_static_files_dir();
1342
1343 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1344 .expect("Failed to create static file provider");
1345
1346 let block_num = 0u64;
1347 let num_slots = 100;
1348 let address = Address::repeat_byte(4u8);
1349
1350 let mut keys: Vec<B256> = Vec::with_capacity(num_slots);
1351 for i in 0..num_slots {
1352 keys.push(B256::with_last_byte(i as u8));
1353 }
1354
1355 {
1356 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1357 let changeset = keys
1358 .iter()
1359 .enumerate()
1360 .map(|(i, key)| StorageBeforeTx { address, key: *key, value: U256::from(i as u64) })
1361 .collect::<Vec<_>>();
1362
1363 writer.append_storage_changeset(changeset, block_num).unwrap();
1364 writer.commit().unwrap();
1365 }
1366
1367 {
1368 let result = sf_rw.get_storage_before_block(block_num, address, keys[0]).unwrap();
1369 assert!(result.is_some());
1370 let entry = result.unwrap();
1371 assert_eq!(entry.key, keys[0]);
1372 assert_eq!(entry.value, U256::ZERO);
1373
1374 let result =
1375 sf_rw.get_storage_before_block(block_num, address, keys[num_slots - 1]).unwrap();
1376 assert!(result.is_some());
1377 let entry = result.unwrap();
1378 assert_eq!(entry.key, keys[num_slots - 1]);
1379
1380 let mid = num_slots / 2;
1381 let result = sf_rw.get_storage_before_block(block_num, address, keys[mid]).unwrap();
1382 assert!(result.is_some());
1383 let entry = result.unwrap();
1384 assert_eq!(entry.key, keys[mid]);
1385
1386 let missing_key = B256::with_last_byte(255);
1387 let result = sf_rw.get_storage_before_block(block_num, address, missing_key).unwrap();
1388 assert!(result.is_none());
1389
1390 for i in (0..num_slots).step_by(10) {
1391 let result = sf_rw.get_storage_before_block(block_num, address, keys[i]).unwrap();
1392 assert!(result.is_some());
1393 assert_eq!(result.unwrap().key, keys[i]);
1394 }
1395 }
1396 }
1397
1398 #[test]
1399 fn test_last_block_flushed_on_commit() {
1400 let (static_dir, _) = create_test_static_files_dir();
1401
1402 let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1403 .expect("Failed to create static file provider");
1404
1405 let address = Address::repeat_byte(5u8);
1406 let key = B256::with_last_byte(1);
1407
1408 {
1411 let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1412
1413 writer
1415 .append_storage_changeset(
1416 vec![StorageBeforeTx { address, key, value: U256::from(42) }],
1417 0,
1418 )
1419 .unwrap();
1420
1421 writer.commit().unwrap();
1423 }
1424
1425 let highest = sf_rw.get_highest_static_file_block(StaticFileSegment::StorageChangeSets);
1427 assert_eq!(highest, Some(0), "Should have block 0 after commit");
1428
1429 let result = sf_rw.get_storage_before_block(0, address, key).unwrap();
1431 assert!(result.is_some(), "Should be able to read the changeset entry");
1432 let entry = result.unwrap();
1433 assert_eq!(entry.value, U256::from(42));
1434 }
1435
1436 #[test]
1437 fn test_read_headers_while_appending() {
1438 let (static_dir, _) = create_test_static_files_dir();
1439 let sf_rw: StaticFileProvider<EthPrimitives> =
1440 StaticFileProviderBuilder::read_write(&static_dir)
1441 .build()
1442 .expect("Failed to build static file provider");
1443 let hash = |num: u64| B256::with_last_byte(num as u8 + 1);
1444
1445 let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
1446 let mut header = Header::default();
1447 for num in 0..=2 {
1448 header.number = num;
1449 header_writer.append_header(&header, &hash(num)).unwrap();
1450 }
1451 header_writer.commit().unwrap();
1452
1453 header.number = 3;
1456 header_writer.append_header(&header, &hash(3)).unwrap();
1457 header_writer.sync_all().unwrap();
1458
1459 sf_rw.initialize_index().unwrap();
1461
1462 assert_eq!(sf_rw.block_hash(2).unwrap(), Some(hash(2)));
1463 assert_eq!(sf_rw.block_hash(3).unwrap(), None);
1464 }
1465
1466 #[test]
1467 fn test_header_read_errors_are_not_missing_blocks() {
1468 for corrupt_offsets in [true, false] {
1469 let (static_dir, _) = create_test_static_files_dir();
1470 let provider = StaticFileProvider::<EthPrimitives>::read_write(&static_dir).unwrap();
1471 let header = Header::default();
1472 let hash = header.hash_slow();
1473 {
1474 let mut writer = provider.latest_writer(StaticFileSegment::Headers).unwrap();
1475 writer.append_header(&header, &hash).unwrap();
1476 writer.commit().unwrap();
1477 }
1478
1479 assert_eq!(provider.header_by_number(0).unwrap(), Some(header));
1480 assert_eq!(provider.block_hash(0).unwrap(), Some(hash));
1481 assert_eq!(provider.header_by_number(1).unwrap(), None);
1482 assert_eq!(provider.block_hash(1).unwrap(), None);
1483
1484 let jar = provider
1485 .get_segment_provider_for_block(StaticFileSegment::Headers, 0, None)
1486 .unwrap();
1487 let path =
1488 if corrupt_offsets { jar.offsets_path() } else { jar.data_path().to_path_buf() };
1489 drop(jar);
1490 drop(provider);
1491
1492 let mut bytes = reth_fs_util::read(&path).unwrap();
1494 if corrupt_offsets {
1495 bytes[1..].fill(u8::MAX);
1497 } else {
1498 bytes.fill(u8::MAX);
1500 }
1501 reth_fs_util::write(&path, bytes).unwrap();
1502
1503 let provider = StaticFileProvider::<EthPrimitives>::read_only(&static_dir).unwrap();
1504 for error in [
1505 provider.header_by_number(0).unwrap_err(),
1506 provider.block_hash(0).unwrap_err(),
1507 provider.sealed_header(0).unwrap_err(),
1508 provider.headers_range(0..1).unwrap_err(),
1509 ] {
1510 let error = error.downcast_other_ref::<NippyJarError>().unwrap();
1511 if corrupt_offsets {
1512 assert_matches!(error, NippyJarError::InconsistentState);
1513 } else {
1514 assert_matches!(error, NippyJarError::OutputTooSmall);
1515 }
1516 }
1517 assert_eq!(provider.header_by_number(1).unwrap(), None);
1518 assert_eq!(provider.block_hash(1).unwrap(), None);
1519 }
1520 }
1521}