Skip to main content

reth_provider/providers/static_file/
mod.rs

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
23/// Alias type for each specific `NippyJar`.
24type LoadedJarRef<'a> =
25    reth_primitives_traits::dashmap::mapref::one::Ref<'a, (u64, StaticFileSegment), LoadedJar>;
26
27/// Helper type to reuse an associated static file mmap handle on created cursors.
28#[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    /// Returns a clone of the mmap handle that can be used to instantiate a cursor.
60    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    /// Returns the total size of the data and offsets files (from the in-memory mmap).
69    fn size(&self) -> usize {
70        self.mmap_handle.size() + self.mmap_handle.offsets_size()
71    }
72
73    /// Returns a reference to the cached changeset offset reader.
74    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        BlockHashReader, ChangeSetReader, ReceiptProvider, StorageChangeSetReader,
108        TransactionsProvider,
109    };
110    use reth_testing_utils::generators::{self, random_header_range};
111    use std::{collections::BTreeMap, fmt::Debug, fs, ops::Range, path::Path};
112
113    fn assert_eyre<T: PartialEq + Debug>(got: T, expected: T, msg: &str) -> eyre::Result<()> {
114        if got != expected {
115            eyre::bail!("{msg} | got: {got:?} expected: {expected:?}");
116        }
117        Ok(())
118    }
119
120    #[test]
121    fn test_static_files() {
122        // Ranges
123        let row_count = 100u64;
124        let range = 0..=(row_count - 1);
125
126        // Data sources
127        let factory = create_test_provider_factory();
128        let static_files_path = tempfile::tempdir().unwrap();
129        let static_file = static_files_path.path().join(
130            StaticFileSegment::Headers
131                .filename(&find_fixed_range(*range.end(), DEFAULT_BLOCKS_PER_STATIC_FILE)),
132        );
133
134        // Setup data
135        let mut headers = random_header_range(
136            &mut generators::rng(),
137            *range.start()..(*range.end() + 1),
138            B256::random(),
139        );
140
141        let mut provider_rw = factory.provider_rw().unwrap();
142        let tx = provider_rw.tx_mut();
143        for header in headers.clone() {
144            let hash = header.hash();
145
146            tx.put::<CanonicalHeaders>(header.number, hash).unwrap();
147            tx.put::<Headers>(header.number, header.clone_header()).unwrap();
148            tx.put::<HeaderNumbers>(hash, header.number).unwrap();
149        }
150        provider_rw.commit().unwrap();
151
152        // Create StaticFile
153        {
154            let manager = factory.static_file_provider();
155            let mut writer = manager.latest_writer(StaticFileSegment::Headers).unwrap();
156
157            for header in headers.clone() {
158                let hash = header.hash();
159                writer.append_header(&header.unseal(), &hash).unwrap();
160            }
161            writer.commit().unwrap();
162        }
163
164        // Use providers to query Header data and compare if it matches
165        {
166            let db_provider = factory.provider().unwrap();
167            let manager = db_provider.static_file_provider();
168            let jar_provider = manager
169                .get_segment_provider_for_block(StaticFileSegment::Headers, 0, Some(&static_file))
170                .unwrap();
171
172            assert!(!headers.is_empty());
173
174            // Shuffled for chaos.
175            headers.shuffle(&mut generators::rng());
176
177            for header in headers {
178                let header_hash = header.hash();
179                let header = header.unseal();
180
181                // Compare Header
182                assert_eq!(header, db_provider.header(header_hash).unwrap().unwrap());
183                assert_eq!(header, jar_provider.header_by_number(header.number).unwrap().unwrap());
184            }
185        }
186    }
187
188    #[test]
189    fn test_header_truncation() {
190        let (static_dir, _) = create_test_static_files_dir();
191
192        let blocks_per_file = 10; // Number of headers per file
193        let files_per_range = 3; // Number of files per range (data/conf/offset files)
194        let file_set_count = 3; // Number of sets of files to create
195        let initial_file_count = files_per_range * file_set_count;
196        let tip = blocks_per_file * file_set_count - 1; // Initial highest block (29 in this case)
197
198        // [ Headers Creation and Commit ]
199        {
200            let sf_rw: StaticFileProvider<EthPrimitives> =
201                StaticFileProviderBuilder::read_write(&static_dir)
202                    .with_blocks_per_file(blocks_per_file)
203                    .build()
204                    .expect("Failed to build static file provider");
205
206            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
207
208            // Append headers from 0 to the tip (29) and commit
209            let mut header = Header::default();
210            for num in 0..=tip {
211                header.number = num;
212                header_writer.append_header(&header, &BlockHash::default()).unwrap();
213            }
214            header_writer.commit().unwrap();
215        }
216
217        // Helper function to prune headers and validate truncation results
218        fn prune_and_validate(
219            writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
220            sf_rw: &StaticFileProvider<EthPrimitives>,
221            static_dir: impl AsRef<Path>,
222            prune_count: u64,
223            expected_tip: Option<u64>,
224            expected_file_count: u64,
225        ) -> eyre::Result<()> {
226            writer.prune_headers(prune_count)?;
227            writer.commit()?;
228
229            // Validate the highest block after pruning
230            assert_eyre(
231                sf_rw.get_highest_static_file_block(StaticFileSegment::Headers),
232                expected_tip,
233                "block mismatch",
234            )?;
235
236            if let Some(id) = expected_tip {
237                assert_eyre(
238                    sf_rw.header_by_number(id)?.map(|h| h.number),
239                    expected_tip,
240                    "header mismatch",
241                )?;
242            }
243
244            // Validate the number of files remaining in the directory
245            assert_eyre(
246                count_files_without_lockfile(static_dir)?,
247                expected_file_count as usize,
248                "file count mismatch",
249            )?;
250
251            Ok(())
252        }
253
254        // [ Test Cases ]
255        type PruneCount = u64;
256        type ExpectedTip = u64;
257        type ExpectedFileCount = u64;
258        let mut tmp_tip = tip;
259        let test_cases: Vec<(PruneCount, Option<ExpectedTip>, ExpectedFileCount)> = vec![
260            // Case 0: Pruning 1 header
261            {
262                tmp_tip -= 1;
263                (1, Some(tmp_tip), initial_file_count)
264            },
265            // Case 1: Pruning remaining rows from file should result in its deletion
266            {
267                tmp_tip -= blocks_per_file - 1;
268                (blocks_per_file - 1, Some(tmp_tip), initial_file_count - files_per_range)
269            },
270            // Case 2: Pruning more headers than a single file has (tip reduced by
271            // blocks_per_file + 1) should result in a file set deletion
272            {
273                tmp_tip -= blocks_per_file + 1;
274                (blocks_per_file + 1, Some(tmp_tip), initial_file_count - files_per_range * 2)
275            },
276            // Case 3: Pruning all remaining headers from the file except the genesis header
277            {
278                (
279                    tmp_tip,
280                    Some(0),         // Only genesis block remains
281                    files_per_range, // The file set with block 0 should remain
282                )
283            },
284            // Case 4: Pruning the genesis header (should not delete the file set with block 0)
285            {
286                (
287                    1,
288                    None,            // No blocks left
289                    files_per_range, // The file set with block 0 remains
290                )
291            },
292        ];
293
294        // Test cases execution
295        {
296            let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
297                .with_blocks_per_file(blocks_per_file)
298                .build()
299                .expect("Failed to build static file provider");
300
301            assert_eq!(sf_rw.get_highest_static_file_block(StaticFileSegment::Headers), Some(tip));
302            assert_eq!(
303                count_files_without_lockfile(static_dir.as_ref()).unwrap(),
304                initial_file_count as usize
305            );
306
307            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
308
309            for (case, (prune_count, expected_tip, expected_file_count)) in
310                test_cases.into_iter().enumerate()
311            {
312                prune_and_validate(
313                    &mut header_writer,
314                    &sf_rw,
315                    &static_dir,
316                    prune_count,
317                    expected_tip,
318                    expected_file_count,
319                )
320                .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
321                .unwrap();
322            }
323        }
324    }
325
326    /// 3 block ranges are built
327    ///
328    /// for `blocks_per_file = 10`:
329    /// * `0..=9` : except genesis, every block has a tx/receipt
330    /// * `10..=19`: no txs/receipts
331    /// * `20..=29`: only one tx/receipt
332    fn setup_tx_based_scenario(
333        sf_rw: &StaticFileProvider<EthPrimitives>,
334        segment: StaticFileSegment,
335        blocks_per_file: u64,
336    ) {
337        fn setup_block_ranges(
338            writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
339            sf_rw: &StaticFileProvider<EthPrimitives>,
340            segment: StaticFileSegment,
341            block_range: &Range<u64>,
342            mut tx_count: u64,
343            next_tx_num: &mut u64,
344        ) {
345            let mut receipt = Receipt::default();
346            let mut tx = TxLegacy::default();
347
348            for block in block_range.clone() {
349                writer.increment_block(block).unwrap();
350
351                // Append transaction/receipt if there's still a transaction count to append
352                if tx_count > 0 {
353                    match segment {
354                        StaticFileSegment::Headers |
355                        StaticFileSegment::AccountChangeSets |
356                        StaticFileSegment::StorageChangeSets => {
357                            panic!("non tx based segment")
358                        }
359                        StaticFileSegment::Transactions => {
360                            // Used as ID for validation
361                            tx.nonce = *next_tx_num;
362                            let tx: TransactionSigned =
363                                tx.clone().into_signed(Signature::test_signature()).into();
364                            writer.append_transaction(*next_tx_num, &tx).unwrap();
365                        }
366                        StaticFileSegment::Receipts => {
367                            // Used as ID for validation
368                            receipt.cumulative_gas_used = *next_tx_num;
369                            writer.append_receipt(*next_tx_num, &receipt).unwrap();
370                        }
371                        StaticFileSegment::TransactionSenders => {
372                            // Used as ID for validation
373                            let sender = Address::from(U160::from(*next_tx_num));
374                            writer.append_transaction_sender(*next_tx_num, &sender).unwrap();
375                        }
376                    }
377                    *next_tx_num += 1;
378                    tx_count -= 1;
379                }
380            }
381            writer.commit().unwrap();
382
383            // Calculate expected values based on the range and transactions
384            let expected_block = block_range.end - 1;
385            let expected_tx = if tx_count == 0 { *next_tx_num - 1 } else { *next_tx_num };
386
387            // Perform assertions after processing the blocks
388            assert_eq!(sf_rw.get_highest_static_file_block(segment), Some(expected_block),);
389            assert_eq!(sf_rw.get_highest_static_file_tx(segment), Some(expected_tx),);
390        }
391
392        // Define the block ranges and transaction counts as vectors
393        let block_ranges = [
394            0..blocks_per_file,
395            blocks_per_file..blocks_per_file * 2,
396            blocks_per_file * 2..blocks_per_file * 3,
397        ];
398
399        let tx_counts = [
400            blocks_per_file - 1, // First range: tx per block except genesis
401            0,                   // Second range: no transactions
402            1,                   // Third range: 1 transaction in the second block
403        ];
404
405        let mut writer = sf_rw.latest_writer(segment).unwrap();
406        let mut next_tx_num = 0;
407
408        // Loop through setup scenarios
409        for (block_range, tx_count) in block_ranges.iter().zip(tx_counts.iter()) {
410            setup_block_ranges(
411                &mut writer,
412                sf_rw,
413                segment,
414                block_range,
415                *tx_count,
416                &mut next_tx_num,
417            );
418        }
419
420        // Ensure that scenario was properly setup
421        let expected_tx_ranges = vec![
422            Some(SegmentRangeInclusive::new(0, 8)),
423            None,
424            Some(SegmentRangeInclusive::new(9, 9)),
425        ];
426
427        block_ranges.iter().zip(expected_tx_ranges).for_each(|(block_range, expected_tx_range)| {
428            assert_eq!(
429                sf_rw
430                    .get_segment_provider_for_block(segment, block_range.start, None)
431                    .unwrap()
432                    .user_header()
433                    .tx_range(),
434                expected_tx_range
435            );
436        });
437
438        // Ensure transaction index
439        let expected_tx_index = BTreeMap::from([
440            (8, SegmentRangeInclusive::new(0, 9)),
441            (9, SegmentRangeInclusive::new(20, 29)),
442        ]);
443        assert_eq!(
444            sf_rw.tx_index(segment),
445            (!expected_tx_index.is_empty()).then_some(expected_tx_index),
446            "tx index mismatch",
447        );
448    }
449
450    #[test]
451    fn test_tx_based_truncation() {
452        let segments = [StaticFileSegment::Transactions, StaticFileSegment::Receipts];
453        let blocks_per_file = 10; // Number of blocks per file
454        let files_per_range = 3; // Number of files per range (data/conf/offset files)
455        let file_set_count = 3; // Number of sets of files to create
456        let initial_file_count = files_per_range * file_set_count;
457
458        #[expect(clippy::too_many_arguments)]
459        fn prune_and_validate(
460            sf_rw: &StaticFileProvider<EthPrimitives>,
461            static_dir: impl AsRef<Path>,
462            segment: StaticFileSegment,
463            prune_count: u64,
464            last_block: u64,
465            expected_tx_tip: Option<u64>,
466            expected_file_count: i32,
467            expected_tx_index: BTreeMap<TxNumber, SegmentRangeInclusive>,
468        ) -> eyre::Result<()> {
469            let mut writer = sf_rw.latest_writer(segment)?;
470
471            // Prune transactions or receipts based on the segment type
472            match segment {
473                StaticFileSegment::Headers |
474                StaticFileSegment::AccountChangeSets |
475                StaticFileSegment::StorageChangeSets => {
476                    panic!("non tx based segment")
477                }
478                StaticFileSegment::Transactions => {
479                    writer.prune_transactions(prune_count, last_block)?
480                }
481                StaticFileSegment::Receipts => writer.prune_receipts(prune_count, last_block)?,
482                StaticFileSegment::TransactionSenders => {
483                    writer.prune_transaction_senders(prune_count, last_block)?
484                }
485            }
486            writer.commit()?;
487
488            // Verify the highest block and transaction tips
489            assert_eyre(
490                sf_rw.get_highest_static_file_block(segment),
491                Some(last_block),
492                "block mismatch",
493            )?;
494            assert_eyre(sf_rw.get_highest_static_file_tx(segment), expected_tx_tip, "tx mismatch")?;
495
496            // Verify that transactions and receipts are returned correctly. Uses
497            // cumulative_gas_used & nonce as ids.
498            if let Some(id) = expected_tx_tip {
499                match segment {
500                    StaticFileSegment::Headers |
501                    StaticFileSegment::AccountChangeSets |
502                    StaticFileSegment::StorageChangeSets => {
503                        panic!("non tx based segment")
504                    }
505                    StaticFileSegment::Transactions => assert_eyre(
506                        expected_tx_tip,
507                        sf_rw.transaction_by_id(id)?.map(|t| t.nonce()),
508                        "tx mismatch",
509                    )?,
510                    StaticFileSegment::Receipts => assert_eyre(
511                        expected_tx_tip,
512                        sf_rw.receipt(id)?.map(|r| r.cumulative_gas_used),
513                        "receipt mismatch",
514                    )?,
515                    StaticFileSegment::TransactionSenders => assert_eyre(
516                        expected_tx_tip,
517                        sf_rw
518                            .transaction_sender(id)?
519                            .map(|s| u64::try_from(U160::from_be_bytes(s.0.into())).unwrap()),
520                        "sender mismatch",
521                    )?,
522                }
523            }
524
525            // Ensure the file count has reduced as expected
526            assert_eyre(
527                count_files_without_lockfile(static_dir)?,
528                expected_file_count as usize,
529                "file count mismatch",
530            )?;
531
532            // Ensure that the inner tx index (max_tx -> block range) is as expected
533            assert_eyre(
534                sf_rw.tx_index(segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()),
535                (!expected_tx_index.is_empty()).then_some(expected_tx_index),
536                "tx index mismatch",
537            )?;
538
539            Ok(())
540        }
541
542        for segment in segments {
543            let (static_dir, _) = create_test_static_files_dir();
544
545            let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
546                .with_blocks_per_file(blocks_per_file)
547                .build()
548                .expect("Failed to build static file provider");
549
550            setup_tx_based_scenario(&sf_rw, segment, blocks_per_file);
551
552            let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
553                .with_blocks_per_file(blocks_per_file)
554                .build()
555                .expect("Failed to build static file provider");
556            let highest_tx = sf_rw.get_highest_static_file_tx(segment).unwrap();
557
558            // Test cases
559            // [prune_count, last_block, expected_tx_tip, expected_file_count, expected_tx_index)
560            let test_cases = vec![
561                // Case 0: 20..=29 has only one tx. Prune the only tx of the block range.
562                // It ensures that the file is not deleted even though there are no rows, since the
563                // `last_block` which is passed to the prune method is the first
564                // block of the range.
565                (
566                    1,
567                    blocks_per_file * 2,
568                    Some(highest_tx - 1),
569                    initial_file_count,
570                    BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
571                ),
572                // Case 1: 10..=19 has no txs. There are no txes in the whole block range, but want
573                // to unwind to block 9. Ensures that the 20..=29 and 10..=19 files
574                // are deleted.
575                (
576                    0,
577                    blocks_per_file - 1,
578                    Some(highest_tx - 1),
579                    files_per_range,
580                    BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
581                ),
582                // Case 2: Prune most txs up to block 1.
583                (
584                    highest_tx - 1,
585                    1,
586                    Some(0),
587                    files_per_range,
588                    BTreeMap::from([(0, SegmentRangeInclusive::new(0, 1))]),
589                ),
590                // Case 3: Prune remaining tx and ensure that file is not deleted.
591                (1, 0, None, files_per_range, BTreeMap::from([])),
592            ];
593
594            // Loop through test cases
595            for (
596                case,
597                (prune_count, last_block, expected_tx_tip, expected_file_count, expected_tx_index),
598            ) in test_cases.into_iter().enumerate()
599            {
600                prune_and_validate(
601                    &sf_rw,
602                    &static_dir,
603                    segment,
604                    prune_count,
605                    last_block,
606                    expected_tx_tip,
607                    expected_file_count,
608                    expected_tx_index,
609                )
610                .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
611                .unwrap();
612            }
613        }
614    }
615
616    /// Returns the number of files in the provided path, excluding ".lock" files.
617    fn count_files_without_lockfile(path: impl AsRef<Path>) -> eyre::Result<usize> {
618        let is_lockfile =
619            |entry: &fs::DirEntry| entry.path().file_name().is_some_and(|name| name == "lock");
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        // Helper function to generate test changesets
719        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                        ..Default::default()
731                    }),
732                })
733                .collect()
734        }
735
736        // Test writing and reading account changesets
737        {
738            let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
739
740            // Create test data for multiple blocks
741            let test_blocks = 10u64;
742            let addresses_per_block = 5;
743
744            for block_num in 0..test_blocks {
745                // Generate unique addresses for each block
746                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);
756
757                writer.append_account_changeset(changeset, block_num).unwrap();
758            }
759
760            writer.commit().unwrap();
761        }
762
763        // Verify data can be read back correctly
764        {
765            let provider = sf_rw
766                .get_segment_provider_for_block(StaticFileSegment::AccountChangeSets, 5, None)
767                .unwrap();
768
769            // Check that the segment header has changeset offsets
770            let offsets = provider.read_changeset_offsets().unwrap();
771            assert!(offsets.is_some());
772            let offsets = offsets.unwrap();
773            assert_eq!(offsets.len(), 10); // Should have 10 blocks worth of offsets
774
775            // Verify each block has the expected number of changes
776            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        // Setup test data
790        let test_address = Address::repeat_byte(1u8);
791        let other_address = Address::repeat_byte(2u8);
792        let missing_address = Address::repeat_byte(3u8);
793
794        // Write changesets for multiple blocks
795        {
796            let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
797
798            // Block 0: test_address and other_address change
799            writer
800                .append_account_changeset(
801                    vec![
802                        AccountBeforeTx {
803                            address: test_address,
804                            info: None, // Account created
805                        },
806                        AccountBeforeTx { address: other_address, info: None },
807                    ],
808                    0,
809                )
810                .unwrap();
811
812            // Block 1: only other_address changes
813            writer
814                .append_account_changeset(
815                    vec![AccountBeforeTx {
816                        address: other_address,
817                        info: Some(Account::default()),
818                    }],
819                    1,
820                )
821                .unwrap();
822
823            // Block 2: test_address changes again
824            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                            ..Default::default()
832                        }),
833                    }],
834                    2,
835                )
836                .unwrap();
837
838            writer.commit().unwrap();
839        }
840
841        // Test get_account_before_block
842        {
843            // Test retrieving account state before block 0
844            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()); // Was created in block 0
849
850            // Test retrieving account state before block 2
851            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            // Test retrieving account that doesn't exist in changeset for block
861            let result = sf_rw.get_account_before_block(1, test_address).unwrap();
862            assert!(result.is_none()); // test_address didn't change in block 1
863
864            // Test retrieving account that never existed
865            let result = sf_rw.get_account_before_block(2, missing_address).unwrap();
866            assert!(result.is_none());
867
868            // Test other_address changes
869            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        // 3 main files (jar, dat, idx) + 1 csoff sidecar file for changeset segments
883        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        // Setup: Create account changesets for multiple blocks
889        {
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                // Create varying number of changes per block
900                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                            ..Default::default()
914                        }),
915                    });
916                }
917
918                writer.append_account_changeset(changeset, block_num).unwrap();
919            }
920
921            writer.commit().unwrap();
922        }
923
924        // Helper function to validate truncation
925        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            // Verify highest block
932            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            // Verify file count
937            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                // Verify we can still read data up to the tip
945                let provider = sf_rw.get_segment_provider_for_block(
946                    StaticFileSegment::AccountChangeSets,
947                    tip,
948                    None,
949                )?;
950
951                // Check offsets are valid
952                let offsets = provider.read_changeset_offsets().unwrap();
953                assert!(offsets.is_some(), "Should have changeset offsets");
954            }
955
956            Ok(())
957        }
958
959        // Test truncation scenarios
960        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        // Re-initialize the index to ensure it knows about the written files
966        sf_rw.initialize_index().expect("Failed to initialize index");
967
968        // Case 1: Truncate to block 20 (remove last 9 blocks)
969        {
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        // Case 2: Truncate to block 9 (should remove 2 files)
979        {
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        // Case 3: Truncate all (should keep block 0)
989        {
990            let mut writer = sf_rw.latest_writer(StaticFileSegment::AccountChangeSets).unwrap();
991            writer.prune_account_changesets(0).unwrap();
992            writer.commit().unwrap();
993
994            // AccountChangeSets behaves like tx-based segments and keeps at least block 0
995            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        // Create a block with many account changes to test binary search
1008        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        // Write the changeset
1020        {
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                        ..Default::default()
1031                    }),
1032                })
1033                .collect();
1034
1035            writer.append_account_changeset(changeset, block_num).unwrap();
1036            writer.commit().unwrap();
1037        }
1038
1039        // Test binary search for various addresses
1040        {
1041            // Test finding first address
1042            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            // Test finding last address
1047            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            // Test finding middle addresses
1053            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            // Test not finding address that doesn't exist
1059            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            // Test multiple lookups for performance
1066            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        // Test writing and reading storage changesets
1082        {
1083            let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1084
1085            // Create test data for multiple blocks
1086            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        // Verify data can be read back correctly
1110        {
1111            let provider = sf_rw
1112                .get_segment_provider_for_block(StaticFileSegment::StorageChangeSets, 5, None)
1113                .unwrap();
1114
1115            // Check that the segment header has changeset offsets
1116            let offsets = provider.read_changeset_offsets().unwrap();
1117            assert!(offsets.is_some());
1118            let offsets = offsets.unwrap();
1119            assert_eq!(offsets.len(), 10); // Should have 10 blocks worth of offsets
1120
1121            // Verify each block has the expected number of changes
1122            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::repeat_byte(1u8);
1136        let other_address = Address::repeat_byte(2u8);
1137        let missing_address = Address::repeat_byte(3u8);
1138        let test_key = B256::with_last_byte(1);
1139        let other_key = B256::with_last_byte(2);
1140
1141        // Write changesets for multiple blocks
1142        {
1143            let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1144
1145            // Block 0: test_address and other_address change
1146            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            // Block 1: only other_address changes
1161            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            // Block 2: test_address changes again
1173            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        // Test get_storage_before_block
1188        {
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        // 3 main files (jar, dat, idx) + 1 csoff sidecar file for changeset segments
1220        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        // Setup: Create storage changesets for multiple blocks
1226        {
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        // Case 1: Truncate to block 20
1294        {
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        // Case 2: Truncate to block 9
1304        {
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        // Case 3: Truncate all (should keep block 0)
1314        {
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::repeat_byte(4u8);
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::ZERO);
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::repeat_byte(5u8);
1391        let key = B256::with_last_byte(1);
1392
1393        // Write changes for a single block without calling increment_block explicitly
1394        // (append_storage_changeset calls it internally), then commit
1395        {
1396            let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1397
1398            // Append a single block's changeset (block 0)
1399            writer
1400                .append_storage_changeset(
1401                    vec![StorageBeforeTx { address, key, value: U256::from(42) }],
1402                    0,
1403                )
1404                .unwrap();
1405
1406            // Commit without any subsequent block - the current block's offset should be flushed
1407            writer.commit().unwrap();
1408        }
1409
1410        // Verify highest block is 0
1411        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        // Verify the data is actually readable via the high-level API
1415        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
1421    #[test]
1422    fn test_read_headers_while_appending() {
1423        let (static_dir, _) = create_test_static_files_dir();
1424        let sf_rw: StaticFileProvider<EthPrimitives> =
1425            StaticFileProviderBuilder::read_write(&static_dir)
1426                .build()
1427                .expect("Failed to build static file provider");
1428        let hash = |num: u64| B256::with_last_byte(num as u8 + 1);
1429
1430        let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
1431        let mut header = Header::default();
1432        for num in 0..=2 {
1433            header.number = num;
1434            header_writer.append_header(&header, &hash(num)).unwrap();
1435        }
1436        header_writer.commit().unwrap();
1437
1438        // Writes the next header to disk without committing it, as persistence does before the
1439        // database commit.
1440        header.number = 3;
1441        header_writer.append_header(&header, &hash(3)).unwrap();
1442        header_writer.sync_all().unwrap();
1443
1444        // Reloads the jar from disk meanwhile, as every static file deletion by the pruner does.
1445        sf_rw.initialize_index().unwrap();
1446
1447        assert_eq!(sf_rw.block_hash(2).unwrap(), Some(hash(2)));
1448        assert_eq!(sf_rw.block_hash(3).unwrap(), None);
1449    }
1450}