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