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        ChangeSetReader, ReceiptProvider, StorageChangeSetReader, TransactionsProvider,
108    };
109    use reth_testing_utils::generators::{self, random_header_range};
110    use std::{collections::BTreeMap, fmt::Debug, fs, ops::Range, path::Path};
111
112    fn assert_eyre<T: PartialEq + Debug>(got: T, expected: T, msg: &str) -> eyre::Result<()> {
113        if got != expected {
114            eyre::bail!("{msg} | got: {got:?} expected: {expected:?}");
115        }
116        Ok(())
117    }
118
119    #[test]
120    fn test_static_files() {
121        // Ranges
122        let row_count = 100u64;
123        let range = 0..=(row_count - 1);
124
125        // Data sources
126        let factory = create_test_provider_factory();
127        let static_files_path = tempfile::tempdir().unwrap();
128        let static_file = static_files_path.path().join(
129            StaticFileSegment::Headers
130                .filename(&find_fixed_range(*range.end(), DEFAULT_BLOCKS_PER_STATIC_FILE)),
131        );
132
133        // Setup data
134        let mut headers = random_header_range(
135            &mut generators::rng(),
136            *range.start()..(*range.end() + 1),
137            B256::random(),
138        );
139
140        let mut provider_rw = factory.provider_rw().unwrap();
141        let tx = provider_rw.tx_mut();
142        for header in headers.clone() {
143            let hash = header.hash();
144
145            tx.put::<CanonicalHeaders>(header.number, hash).unwrap();
146            tx.put::<Headers>(header.number, header.clone_header()).unwrap();
147            tx.put::<HeaderNumbers>(hash, header.number).unwrap();
148        }
149        provider_rw.commit().unwrap();
150
151        // Create StaticFile
152        {
153            let manager = factory.static_file_provider();
154            let mut writer = manager.latest_writer(StaticFileSegment::Headers).unwrap();
155
156            for header in headers.clone() {
157                let hash = header.hash();
158                writer.append_header(&header.unseal(), &hash).unwrap();
159            }
160            writer.commit().unwrap();
161        }
162
163        // Use providers to query Header data and compare if it matches
164        {
165            let db_provider = factory.provider().unwrap();
166            let manager = db_provider.static_file_provider();
167            let jar_provider = manager
168                .get_segment_provider_for_block(StaticFileSegment::Headers, 0, Some(&static_file))
169                .unwrap();
170
171            assert!(!headers.is_empty());
172
173            // Shuffled for chaos.
174            headers.shuffle(&mut generators::rng());
175
176            for header in headers {
177                let header_hash = header.hash();
178                let header = header.unseal();
179
180                // Compare Header
181                assert_eq!(header, db_provider.header(header_hash).unwrap().unwrap());
182                assert_eq!(header, jar_provider.header_by_number(header.number).unwrap().unwrap());
183            }
184        }
185    }
186
187    #[test]
188    fn test_header_truncation() {
189        let (static_dir, _) = create_test_static_files_dir();
190
191        let blocks_per_file = 10; // Number of headers per file
192        let files_per_range = 3; // Number of files per range (data/conf/offset files)
193        let file_set_count = 3; // Number of sets of files to create
194        let initial_file_count = files_per_range * file_set_count;
195        let tip = blocks_per_file * file_set_count - 1; // Initial highest block (29 in this case)
196
197        // [ Headers Creation and Commit ]
198        {
199            let sf_rw: StaticFileProvider<EthPrimitives> =
200                StaticFileProviderBuilder::read_write(&static_dir)
201                    .with_blocks_per_file(blocks_per_file)
202                    .build()
203                    .expect("Failed to build static file provider");
204
205            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
206
207            // Append headers from 0 to the tip (29) and commit
208            let mut header = Header::default();
209            for num in 0..=tip {
210                header.number = num;
211                header_writer.append_header(&header, &BlockHash::default()).unwrap();
212            }
213            header_writer.commit().unwrap();
214        }
215
216        // Helper function to prune headers and validate truncation results
217        fn prune_and_validate(
218            writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
219            sf_rw: &StaticFileProvider<EthPrimitives>,
220            static_dir: impl AsRef<Path>,
221            prune_count: u64,
222            expected_tip: Option<u64>,
223            expected_file_count: u64,
224        ) -> eyre::Result<()> {
225            writer.prune_headers(prune_count)?;
226            writer.commit()?;
227
228            // Validate the highest block after pruning
229            assert_eyre(
230                sf_rw.get_highest_static_file_block(StaticFileSegment::Headers),
231                expected_tip,
232                "block mismatch",
233            )?;
234
235            if let Some(id) = expected_tip {
236                assert_eyre(
237                    sf_rw.header_by_number(id)?.map(|h| h.number),
238                    expected_tip,
239                    "header mismatch",
240                )?;
241            }
242
243            // Validate the number of files remaining in the directory
244            assert_eyre(
245                count_files_without_lockfile(static_dir)?,
246                expected_file_count as usize,
247                "file count mismatch",
248            )?;
249
250            Ok(())
251        }
252
253        // [ Test Cases ]
254        type PruneCount = u64;
255        type ExpectedTip = u64;
256        type ExpectedFileCount = u64;
257        let mut tmp_tip = tip;
258        let test_cases: Vec<(PruneCount, Option<ExpectedTip>, ExpectedFileCount)> = vec![
259            // Case 0: Pruning 1 header
260            {
261                tmp_tip -= 1;
262                (1, Some(tmp_tip), initial_file_count)
263            },
264            // Case 1: Pruning remaining rows from file should result in its deletion
265            {
266                tmp_tip -= blocks_per_file - 1;
267                (blocks_per_file - 1, Some(tmp_tip), initial_file_count - files_per_range)
268            },
269            // Case 2: Pruning more headers than a single file has (tip reduced by
270            // blocks_per_file + 1) should result in a file set deletion
271            {
272                tmp_tip -= blocks_per_file + 1;
273                (blocks_per_file + 1, Some(tmp_tip), initial_file_count - files_per_range * 2)
274            },
275            // Case 3: Pruning all remaining headers from the file except the genesis header
276            {
277                (
278                    tmp_tip,
279                    Some(0),         // Only genesis block remains
280                    files_per_range, // The file set with block 0 should remain
281                )
282            },
283            // Case 4: Pruning the genesis header (should not delete the file set with block 0)
284            {
285                (
286                    1,
287                    None,            // No blocks left
288                    files_per_range, // The file set with block 0 remains
289                )
290            },
291        ];
292
293        // Test cases execution
294        {
295            let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
296                .with_blocks_per_file(blocks_per_file)
297                .build()
298                .expect("Failed to build static file provider");
299
300            assert_eq!(sf_rw.get_highest_static_file_block(StaticFileSegment::Headers), Some(tip));
301            assert_eq!(
302                count_files_without_lockfile(static_dir.as_ref()).unwrap(),
303                initial_file_count as usize
304            );
305
306            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers).unwrap();
307
308            for (case, (prune_count, expected_tip, expected_file_count)) in
309                test_cases.into_iter().enumerate()
310            {
311                prune_and_validate(
312                    &mut header_writer,
313                    &sf_rw,
314                    &static_dir,
315                    prune_count,
316                    expected_tip,
317                    expected_file_count,
318                )
319                .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
320                .unwrap();
321            }
322        }
323    }
324
325    /// 3 block ranges are built
326    ///
327    /// for `blocks_per_file = 10`:
328    /// * `0..=9` : except genesis, every block has a tx/receipt
329    /// * `10..=19`: no txs/receipts
330    /// * `20..=29`: only one tx/receipt
331    fn setup_tx_based_scenario(
332        sf_rw: &StaticFileProvider<EthPrimitives>,
333        segment: StaticFileSegment,
334        blocks_per_file: u64,
335    ) {
336        fn setup_block_ranges(
337            writer: &mut StaticFileProviderRWRefMut<'_, EthPrimitives>,
338            sf_rw: &StaticFileProvider<EthPrimitives>,
339            segment: StaticFileSegment,
340            block_range: &Range<u64>,
341            mut tx_count: u64,
342            next_tx_num: &mut u64,
343        ) {
344            let mut receipt = Receipt::default();
345            let mut tx = TxLegacy::default();
346
347            for block in block_range.clone() {
348                writer.increment_block(block).unwrap();
349
350                // Append transaction/receipt if there's still a transaction count to append
351                if tx_count > 0 {
352                    match segment {
353                        StaticFileSegment::Headers |
354                        StaticFileSegment::AccountChangeSets |
355                        StaticFileSegment::StorageChangeSets => {
356                            panic!("non tx based segment")
357                        }
358                        StaticFileSegment::Transactions => {
359                            // Used as ID for validation
360                            tx.nonce = *next_tx_num;
361                            let tx: TransactionSigned =
362                                tx.clone().into_signed(Signature::test_signature()).into();
363                            writer.append_transaction(*next_tx_num, &tx).unwrap();
364                        }
365                        StaticFileSegment::Receipts => {
366                            // Used as ID for validation
367                            receipt.cumulative_gas_used = *next_tx_num;
368                            writer.append_receipt(*next_tx_num, &receipt).unwrap();
369                        }
370                        StaticFileSegment::TransactionSenders => {
371                            // Used as ID for validation
372                            let sender = Address::from(U160::from(*next_tx_num));
373                            writer.append_transaction_sender(*next_tx_num, &sender).unwrap();
374                        }
375                    }
376                    *next_tx_num += 1;
377                    tx_count -= 1;
378                }
379            }
380            writer.commit().unwrap();
381
382            // Calculate expected values based on the range and transactions
383            let expected_block = block_range.end - 1;
384            let expected_tx = if tx_count == 0 { *next_tx_num - 1 } else { *next_tx_num };
385
386            // Perform assertions after processing the blocks
387            assert_eq!(sf_rw.get_highest_static_file_block(segment), Some(expected_block),);
388            assert_eq!(sf_rw.get_highest_static_file_tx(segment), Some(expected_tx),);
389        }
390
391        // Define the block ranges and transaction counts as vectors
392        let block_ranges = [
393            0..blocks_per_file,
394            blocks_per_file..blocks_per_file * 2,
395            blocks_per_file * 2..blocks_per_file * 3,
396        ];
397
398        let tx_counts = [
399            blocks_per_file - 1, // First range: tx per block except genesis
400            0,                   // Second range: no transactions
401            1,                   // Third range: 1 transaction in the second block
402        ];
403
404        let mut writer = sf_rw.latest_writer(segment).unwrap();
405        let mut next_tx_num = 0;
406
407        // Loop through setup scenarios
408        for (block_range, tx_count) in block_ranges.iter().zip(tx_counts.iter()) {
409            setup_block_ranges(
410                &mut writer,
411                sf_rw,
412                segment,
413                block_range,
414                *tx_count,
415                &mut next_tx_num,
416            );
417        }
418
419        // Ensure that scenario was properly setup
420        let expected_tx_ranges = vec![
421            Some(SegmentRangeInclusive::new(0, 8)),
422            None,
423            Some(SegmentRangeInclusive::new(9, 9)),
424        ];
425
426        block_ranges.iter().zip(expected_tx_ranges).for_each(|(block_range, expected_tx_range)| {
427            assert_eq!(
428                sf_rw
429                    .get_segment_provider_for_block(segment, block_range.start, None)
430                    .unwrap()
431                    .user_header()
432                    .tx_range(),
433                expected_tx_range
434            );
435        });
436
437        // Ensure transaction index
438        let expected_tx_index = BTreeMap::from([
439            (8, SegmentRangeInclusive::new(0, 9)),
440            (9, SegmentRangeInclusive::new(20, 29)),
441        ]);
442        assert_eq!(
443            sf_rw.tx_index(segment),
444            (!expected_tx_index.is_empty()).then_some(expected_tx_index),
445            "tx index mismatch",
446        );
447    }
448
449    #[test]
450    fn test_tx_based_truncation() {
451        let segments = [StaticFileSegment::Transactions, StaticFileSegment::Receipts];
452        let blocks_per_file = 10; // Number of blocks per file
453        let files_per_range = 3; // Number of files per range (data/conf/offset files)
454        let file_set_count = 3; // Number of sets of files to create
455        let initial_file_count = files_per_range * file_set_count;
456
457        #[expect(clippy::too_many_arguments)]
458        fn prune_and_validate(
459            sf_rw: &StaticFileProvider<EthPrimitives>,
460            static_dir: impl AsRef<Path>,
461            segment: StaticFileSegment,
462            prune_count: u64,
463            last_block: u64,
464            expected_tx_tip: Option<u64>,
465            expected_file_count: i32,
466            expected_tx_index: BTreeMap<TxNumber, SegmentRangeInclusive>,
467        ) -> eyre::Result<()> {
468            let mut writer = sf_rw.latest_writer(segment)?;
469
470            // Prune transactions or receipts based on the segment type
471            match segment {
472                StaticFileSegment::Headers |
473                StaticFileSegment::AccountChangeSets |
474                StaticFileSegment::StorageChangeSets => {
475                    panic!("non tx based segment")
476                }
477                StaticFileSegment::Transactions => {
478                    writer.prune_transactions(prune_count, last_block)?
479                }
480                StaticFileSegment::Receipts => writer.prune_receipts(prune_count, last_block)?,
481                StaticFileSegment::TransactionSenders => {
482                    writer.prune_transaction_senders(prune_count, last_block)?
483                }
484            }
485            writer.commit()?;
486
487            // Verify the highest block and transaction tips
488            assert_eyre(
489                sf_rw.get_highest_static_file_block(segment),
490                Some(last_block),
491                "block mismatch",
492            )?;
493            assert_eyre(sf_rw.get_highest_static_file_tx(segment), expected_tx_tip, "tx mismatch")?;
494
495            // Verify that transactions and receipts are returned correctly. Uses
496            // cumulative_gas_used & nonce as ids.
497            if let Some(id) = expected_tx_tip {
498                match segment {
499                    StaticFileSegment::Headers |
500                    StaticFileSegment::AccountChangeSets |
501                    StaticFileSegment::StorageChangeSets => {
502                        panic!("non tx based segment")
503                    }
504                    StaticFileSegment::Transactions => assert_eyre(
505                        expected_tx_tip,
506                        sf_rw.transaction_by_id(id)?.map(|t| t.nonce()),
507                        "tx mismatch",
508                    )?,
509                    StaticFileSegment::Receipts => assert_eyre(
510                        expected_tx_tip,
511                        sf_rw.receipt(id)?.map(|r| r.cumulative_gas_used),
512                        "receipt mismatch",
513                    )?,
514                    StaticFileSegment::TransactionSenders => assert_eyre(
515                        expected_tx_tip,
516                        sf_rw
517                            .transaction_sender(id)?
518                            .map(|s| u64::try_from(U160::from_be_bytes(s.0.into())).unwrap()),
519                        "sender mismatch",
520                    )?,
521                }
522            }
523
524            // Ensure the file count has reduced as expected
525            assert_eyre(
526                count_files_without_lockfile(static_dir)?,
527                expected_file_count as usize,
528                "file count mismatch",
529            )?;
530
531            // Ensure that the inner tx index (max_tx -> block range) is as expected
532            assert_eyre(
533                sf_rw.tx_index(segment).map(|index| index.iter().map(|(k, v)| (*k, *v)).collect()),
534                (!expected_tx_index.is_empty()).then_some(expected_tx_index),
535                "tx index mismatch",
536            )?;
537
538            Ok(())
539        }
540
541        for segment in segments {
542            let (static_dir, _) = create_test_static_files_dir();
543
544            let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
545                .with_blocks_per_file(blocks_per_file)
546                .build()
547                .expect("Failed to build static file provider");
548
549            setup_tx_based_scenario(&sf_rw, segment, blocks_per_file);
550
551            let sf_rw = StaticFileProviderBuilder::read_write(&static_dir)
552                .with_blocks_per_file(blocks_per_file)
553                .build()
554                .expect("Failed to build static file provider");
555            let highest_tx = sf_rw.get_highest_static_file_tx(segment).unwrap();
556
557            // Test cases
558            // [prune_count, last_block, expected_tx_tip, expected_file_count, expected_tx_index)
559            let test_cases = vec![
560                // Case 0: 20..=29 has only one tx. Prune the only tx of the block range.
561                // It ensures that the file is not deleted even though there are no rows, since the
562                // `last_block` which is passed to the prune method is the first
563                // block of the range.
564                (
565                    1,
566                    blocks_per_file * 2,
567                    Some(highest_tx - 1),
568                    initial_file_count,
569                    BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
570                ),
571                // Case 1: 10..=19 has no txs. There are no txes in the whole block range, but want
572                // to unwind to block 9. Ensures that the 20..=29 and 10..=19 files
573                // are deleted.
574                (
575                    0,
576                    blocks_per_file - 1,
577                    Some(highest_tx - 1),
578                    files_per_range,
579                    BTreeMap::from([(highest_tx - 1, SegmentRangeInclusive::new(0, 9))]),
580                ),
581                // Case 2: Prune most txs up to block 1.
582                (
583                    highest_tx - 1,
584                    1,
585                    Some(0),
586                    files_per_range,
587                    BTreeMap::from([(0, SegmentRangeInclusive::new(0, 1))]),
588                ),
589                // Case 3: Prune remaining tx and ensure that file is not deleted.
590                (1, 0, None, files_per_range, BTreeMap::from([])),
591            ];
592
593            // Loop through test cases
594            for (
595                case,
596                (prune_count, last_block, expected_tx_tip, expected_file_count, expected_tx_index),
597            ) in test_cases.into_iter().enumerate()
598            {
599                prune_and_validate(
600                    &sf_rw,
601                    &static_dir,
602                    segment,
603                    prune_count,
604                    last_block,
605                    expected_tx_tip,
606                    expected_file_count,
607                    expected_tx_index,
608                )
609                .map_err(|err| eyre::eyre!("Test case {case}: {err}"))
610                .unwrap();
611            }
612        }
613    }
614
615    /// Returns the number of files in the provided path, excluding ".lock" files.
616    fn count_files_without_lockfile(path: impl AsRef<Path>) -> eyre::Result<usize> {
617        let is_lockfile = |entry: &fs::DirEntry| {
618            entry.path().file_name().map(|name| name == "lock").unwrap_or(false)
619        };
620        let count = fs::read_dir(path)?
621            .filter_map(|entry| entry.ok())
622            .filter(|entry| !is_lockfile(entry))
623            .count();
624
625        Ok(count)
626    }
627
628    #[test]
629    fn test_dynamic_size() -> eyre::Result<()> {
630        let (static_dir, _) = create_test_static_files_dir();
631
632        {
633            let sf_rw: StaticFileProvider<EthPrimitives> =
634                StaticFileProviderBuilder::read_write(&static_dir)
635                    .with_blocks_per_file(10)
636                    .build()?;
637            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
638
639            let mut header = Header::default();
640            for num in 0..=15 {
641                header.number = num;
642                header_writer.append_header(&header, &BlockHash::default()).unwrap();
643            }
644            header_writer.commit().unwrap();
645
646            assert_eq!(sf_rw.headers_range(0..=15)?.len(), 16);
647            assert_eq!(
648                sf_rw.expected_block_index(StaticFileSegment::Headers),
649                Some(BTreeMap::from([
650                    (9, SegmentRangeInclusive::new(0, 9)),
651                    (19, SegmentRangeInclusive::new(10, 19))
652                ])),
653            )
654        }
655
656        {
657            let sf_rw: StaticFileProvider<EthPrimitives> =
658                StaticFileProviderBuilder::read_write(&static_dir)
659                    .with_blocks_per_file(5)
660                    .build()?;
661            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
662
663            let mut header = Header::default();
664            for num in 16..=22 {
665                header.number = num;
666                header_writer.append_header(&header, &BlockHash::default()).unwrap();
667            }
668            header_writer.commit().unwrap();
669
670            assert_eq!(sf_rw.headers_range(0..=22)?.len(), 23);
671            assert_eq!(
672                sf_rw.expected_block_index(StaticFileSegment::Headers),
673                Some(BTreeMap::from([
674                    (9, SegmentRangeInclusive::new(0, 9)),
675                    (19, SegmentRangeInclusive::new(10, 19)),
676                    (24, SegmentRangeInclusive::new(20, 24))
677                ]))
678            )
679        }
680
681        {
682            let sf_rw: StaticFileProvider<EthPrimitives> =
683                StaticFileProviderBuilder::read_write(&static_dir)
684                    .with_blocks_per_file(15)
685                    .build()?;
686            let mut header_writer = sf_rw.latest_writer(StaticFileSegment::Headers)?;
687
688            let mut header = Header::default();
689            for num in 23..=40 {
690                header.number = num;
691                header_writer.append_header(&header, &BlockHash::default()).unwrap();
692            }
693            header_writer.commit().unwrap();
694
695            assert_eq!(sf_rw.headers_range(0..=40)?.len(), 41);
696            assert_eq!(
697                sf_rw.expected_block_index(StaticFileSegment::Headers),
698                Some(BTreeMap::from([
699                    (9, SegmentRangeInclusive::new(0, 9)),
700                    (19, SegmentRangeInclusive::new(10, 19)),
701                    (24, SegmentRangeInclusive::new(20, 24)),
702                    (39, SegmentRangeInclusive::new(25, 39)),
703                    (54, SegmentRangeInclusive::new(40, 54))
704                ]))
705            )
706        }
707
708        Ok(())
709    }
710
711    #[test]
712    fn test_account_changeset_static_files() {
713        let (static_dir, _) = create_test_static_files_dir();
714
715        let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
716            .expect("Failed to create static file provider");
717
718        // 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                        bytecode_hash: None,
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.clone());
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::from([1u8; 20]);
791        let other_address = Address::from([2u8; 20]);
792        let missing_address = Address::from([3u8; 20]);
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 { nonce: 0, balance: U256::ZERO, bytecode_hash: None }),
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                            bytecode_hash: None,
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                            bytecode_hash: None,
914                        }),
915                    });
916                }
917
918                writer.append_account_changeset(changeset, block_num).unwrap();
919            }
920
921            writer.commit().unwrap();
922        }
923
924        // 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                        bytecode_hash: None,
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::from([1u8; 20]);
1136        let other_address = Address::from([2u8; 20]);
1137        let missing_address = Address::from([3u8; 20]);
1138        let test_key = B256::with_last_byte(1);
1139        let other_key = B256::with_last_byte(2);
1140
1141        // 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::from([4u8; 20]);
1334
1335        let mut keys: Vec<B256> = Vec::with_capacity(num_slots);
1336        for i in 0..num_slots {
1337            keys.push(B256::with_last_byte(i as u8));
1338        }
1339
1340        {
1341            let mut writer = sf_rw.latest_writer(StaticFileSegment::StorageChangeSets).unwrap();
1342            let changeset = keys
1343                .iter()
1344                .enumerate()
1345                .map(|(i, key)| StorageBeforeTx { address, key: *key, value: U256::from(i as u64) })
1346                .collect::<Vec<_>>();
1347
1348            writer.append_storage_changeset(changeset, block_num).unwrap();
1349            writer.commit().unwrap();
1350        }
1351
1352        {
1353            let result = sf_rw.get_storage_before_block(block_num, address, keys[0]).unwrap();
1354            assert!(result.is_some());
1355            let entry = result.unwrap();
1356            assert_eq!(entry.key, keys[0]);
1357            assert_eq!(entry.value, U256::from(0));
1358
1359            let result =
1360                sf_rw.get_storage_before_block(block_num, address, keys[num_slots - 1]).unwrap();
1361            assert!(result.is_some());
1362            let entry = result.unwrap();
1363            assert_eq!(entry.key, keys[num_slots - 1]);
1364
1365            let mid = num_slots / 2;
1366            let result = sf_rw.get_storage_before_block(block_num, address, keys[mid]).unwrap();
1367            assert!(result.is_some());
1368            let entry = result.unwrap();
1369            assert_eq!(entry.key, keys[mid]);
1370
1371            let missing_key = B256::with_last_byte(255);
1372            let result = sf_rw.get_storage_before_block(block_num, address, missing_key).unwrap();
1373            assert!(result.is_none());
1374
1375            for i in (0..num_slots).step_by(10) {
1376                let result = sf_rw.get_storage_before_block(block_num, address, keys[i]).unwrap();
1377                assert!(result.is_some());
1378                assert_eq!(result.unwrap().key, keys[i]);
1379            }
1380        }
1381    }
1382
1383    #[test]
1384    fn test_last_block_flushed_on_commit() {
1385        let (static_dir, _) = create_test_static_files_dir();
1386
1387        let sf_rw = StaticFileProvider::<EthPrimitives>::read_write(&static_dir)
1388            .expect("Failed to create static file provider");
1389
1390        let address = Address::from([5u8; 20]);
1391        let key = B256::with_last_byte(1);
1392
1393        // 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}