diff --git a/crates/storage/provider/src/providers/static_file/writer.rs b/crates/storage/provider/src/providers/static_file/writer.rs index a6dbb379c17..bc6d1dd755a 100644 --- a/crates/storage/provider/src/providers/static_file/writer.rs +++ b/crates/storage/provider/src/providers/static_file/writer.rs @@ -704,6 +704,14 @@ impl StaticFileProviderRW { // // 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. + // + // We also skip this heuristic when there are no existing static files for this segment. + // This handles migration (e.g. v1→v2) where we open a writer starting at a non-zero block + // but there is no previous file on disk. Without this guard, `StaticFileProviderInner:: + // update_index` would try to load the (non-existent) previous range's `.conf` file and + // return an ENOENT error. + let segment = self.writer.user_header().segment(); + let reader = self.reader(); let segment_max_block = self .writer .user_header() @@ -711,12 +719,13 @@ 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(); + let has_previous_files = reader.get_highest_static_file_block(segment).is_some(); + (expected_start > reader.genesis_block_number() && has_previous_files) + .then(|| expected_start - 1) }); - self.reader().update_index(self.writer.user_header().segment(), segment_max_block) + 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 ee5182a2005..cf2ac3c91ee 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,39 @@ mod tests { "Should have 7 blocks * 5 changes = 35 rows" ); } + + /// Regression test for . + /// + /// When `migrate-v2` is run on a pruned full node the `AccountChangeSets` segment starts + /// at a non-zero block (the block after the prune checkpoint). If that starting block is the + /// first block of a new static-file range, `update_index` used to compute + /// `segment_max_block = expected_block_start - 1`, which falls in the *previous* range. + /// It then tried to `NippyJar::load` that previous range's file — which never existed — and + /// returned ENOENT. + /// + /// The fix: skip the "previous file" heuristic when no static files for the segment exist yet. + #[test] + fn test_get_writer_at_range_boundary_without_previous_file() { + // Use blocks_per_file=10 so block 10 is the first block of the second range (10..=19). + // We never write anything to range 0..=9, mimicking a pruned node where those + // changesets were pruned before migration. + let (static_dir, _) = create_test_static_files_dir(); + let provider = setup_test_provider(&static_dir, 10); + + // This must not fail with "No such file or directory" for range 0..=9. + let mut writer = provider + .get_writer(10, StaticFileSegment::AccountChangeSets) + .expect("get_writer at range boundary should succeed when no previous file exists"); + + // Write a block and commit to confirm the writer is fully functional. + let changeset = generate_test_changeset(10, 3); + writer.append_account_changeset(changeset, 10).unwrap(); + writer.commit().unwrap(); + + // The index should reflect block 10 as the current highest. + assert_eq!( + provider.get_highest_static_file_block(StaticFileSegment::AccountChangeSets), + Some(10), + ); + } }