diff --git a/crates/cli/commands/src/db/migrate_v2.rs b/crates/cli/commands/src/db/migrate_v2.rs index 61268f72032..072fe0d419e 100644 --- a/crates/cli/commands/src/db/migrate_v2.rs +++ b/crates/cli/commands/src/db/migrate_v2.rs @@ -5,6 +5,7 @@ //! state), compacts MDBX, then runs the pipeline to rebuild them. use crate::common::CliNodeTypes; +use alloy_primitives::Address; use clap::Parser; use reth_db::{ mdbx::{self, ffi}, @@ -132,8 +133,12 @@ impl Command { .and_then(|cp| cp.block_number) .map_or(0, |b| b + 1); - let mut writer = - sf_provider.get_writer(first_block, StaticFileSegment::AccountChangeSets)?; + // The writer always starts at the fixed range boundary (e.g. 2500000) which may be + // earlier than first_block (e.g. 2603897 from prune checkpoint). + let mut writer = sf_provider.latest_writer(StaticFileSegment::AccountChangeSets)?; + if first_block > 0 { + writer.ensure_at_block(first_block - 1)?; + } let mut count = 0u64; let mut walker = cursor.walk(Some(first_block))?.peekable(); @@ -174,11 +179,15 @@ impl Command { .and_then(|cp| cp.block_number) .map_or(0, |b| b + 1); - let mut writer = - sf_provider.get_writer(first_block, StaticFileSegment::StorageChangeSets)?; + // The writer always starts at the fixed range boundary (e.g. 2500000) which may be + // earlier than first_block (e.g. 2603897 from prune checkpoint). + let mut writer = sf_provider.latest_writer(StaticFileSegment::StorageChangeSets)?; + if first_block > 0 { + writer.ensure_at_block(first_block - 1)?; + } let mut count = 0u64; - let mut walker = cursor.walk(Some(Default::default()))?.peekable(); + let mut walker = cursor.walk(Some((first_block, Address::ZERO).into()))?.peekable(); for block in first_block..=tip { let mut entries = Vec::new(); @@ -238,6 +247,18 @@ impl Command { .map_or(0, |b| b + 1); let first_block = prune_start.max(existing.map_or(0, |b| b + 1)); + // The writer always starts at the fixed range boundary (e.g. 2500000) which may be + // earlier than first_block (e.g. 2603897 from prune checkpoint). + if first_block > 0 { + let mut writer = sf_provider.latest_writer(StaticFileSegment::Receipts)?; + writer.ensure_at_block(first_block - 1)?; + writer.commit()?; + } + + let before = sf_provider + .get_highest_static_file_tx(StaticFileSegment::Receipts) + .map_or(0, |tx| tx + 1); + let block_range = first_block..=tip; let segment = reth_static_file::segments::Receipts; @@ -245,7 +266,11 @@ impl Command { sf_provider.commit()?; - info!(target: "reth::cli", "Receipts migrated"); + let after = sf_provider + .get_highest_static_file_tx(StaticFileSegment::Receipts) + .map_or(0, |tx| tx + 1); + let count = after - before; + info!(target: "reth::cli", count, "Receipts migrated"); Ok(()) } diff --git a/crates/storage/provider/src/providers/static_file/writer.rs b/crates/storage/provider/src/providers/static_file/writer.rs index 5a022bef1f0..c4a9d4f1871 100644 --- a/crates/storage/provider/src/providers/static_file/writer.rs +++ b/crates/storage/provider/src/providers/static_file/writer.rs @@ -696,14 +696,15 @@ impl StaticFileProviderRW { /// Updates the `self.reader` internal index. fn update_index(&self) -> ProviderResult<()> { + let segment = self.writer.user_header().segment(); + // We find the maximum block of the segment by checking this writer's last block. // // However if there's no block range (because there's no data), we try to calculate it by // subtracting 1 from the expected block start, resulting on the last block of the - // previous file. - // - // If that expected block start is 0, then it means that there's no actual block data, and - // there's no block data in static files. + // previous file — but only if that file actually exists. If the previous file doesn't + // exist (e.g. first-ever file for a segment starting past range boundary), there's + // nothing to index. let segment_max_block = self .writer .user_header() @@ -711,12 +712,18 @@ impl StaticFileProviderRW { .as_ref() .map(|block_range| block_range.end()) .or_else(|| { - (self.writer.user_header().expected_block_start() > - self.reader().genesis_block_number()) - .then(|| self.writer.user_header().expected_block_start() - 1) + let expected_start = self.writer.user_header().expected_block_start(); + if expected_start <= self.reader().genesis_block_number() { + return None; + } + + let prev_block = expected_start - 1; + let prev_range = self.reader().find_fixed_range(segment, prev_block); + let prev_path = self.reader().directory().join(segment.filename(&prev_range)); + prev_path.exists().then_some(prev_block) }); - self.reader().update_index(self.writer.user_header().segment(), segment_max_block) + self.reader().update_index(segment, segment_max_block) } /// Ensures that the writer is positioned at the specified block number. diff --git a/crates/storage/provider/src/providers/static_file/writer_tests.rs b/crates/storage/provider/src/providers/static_file/writer_tests.rs index 8f75ea5529e..bc0575f0fb3 100644 --- a/crates/storage/provider/src/providers/static_file/writer_tests.rs +++ b/crates/storage/provider/src/providers/static_file/writer_tests.rs @@ -809,4 +809,78 @@ mod tests { "Should have 7 blocks * 5 changes = 35 rows" ); } + + /// Opening a writer for a block past the first range boundary should succeed + /// even when no previous static file exists for the segment. + #[test] + fn test_get_writer_no_previous_file() { + let (static_dir, _) = create_test_static_files_dir(); + let provider = setup_test_provider(&static_dir, 100); + + // Request a writer starting at block 250, which falls into range 200..=299. + // No file exists for range 100..=199 (the "previous" range). + // This must not panic or error. + let mut writer = provider + .get_writer(250, StaticFileSegment::AccountChangeSets) + .expect("get_writer should succeed without previous file"); + + // The index should have no entry for AccountChangeSets yet (empty jar). + assert!( + provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).is_none(), + "Empty jar should not create an index entry" + ); + + // Writing data requires padding from the range start (200) to block 250, + // same as the migration code does. + let writer_start = writer.next_block_number(); + for block in writer_start..250 { + writer.append_account_changeset(vec![], block).unwrap(); + } + let changeset = generate_test_changeset(250, 2); + writer.append_account_changeset(changeset, 250).unwrap(); + writer.commit().unwrap(); + + assert_eq!( + provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).unwrap(), + 250, + "After writing block 250, highest block should be 250" + ); + } + + /// When a previous file DOES exist, opening a new empty writer for the next + /// range should still update the index to point at the previous file. + #[test] + fn test_get_writer_with_previous_file() { + let (static_dir, _) = create_test_static_files_dir(); + let provider = setup_test_provider(&static_dir, 100); + + // Write blocks 0..=99 to fill the first file completely. + { + let mut writer = provider.get_writer(0, StaticFileSegment::AccountChangeSets).unwrap(); + for block in 0..100 { + writer.append_account_changeset(generate_test_changeset(block, 1), block).unwrap(); + } + writer.commit().unwrap(); + } + + assert_eq!( + provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).unwrap(), + 99 + ); + + // Now get a writer for block 100 (next range 100..=199). + // The previous file (0..=99) exists, so this should succeed. + let writer = provider + .get_writer(100, StaticFileSegment::AccountChangeSets) + .expect("get_writer should succeed with previous file"); + + // The index should still reflect the previous file's max block. + assert_eq!( + provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets).unwrap(), + 99, + "Index should still point at previous file's max block" + ); + + drop(writer); + } }