From ab10772b2b75903468db68a8ed480b781e9a6b95 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Thu, 23 Apr 2026 17:01:38 +0100 Subject: [PATCH 1/7] fix(storage): fix migrate-v2 for pruned nodes writer.update_index() synthesized segment_max_block = expected_block_start - 1 for empty jars, causing manager.update_index() to NippyJar::load a previous range file that may not exist. Check that the previous file actually exists before referencing it; if not, pass None (no-op for the index). Also pad empty changesets/blocks in migrate-v2 when the prune checkpoint is past the static file range boundary. --- crates/cli/commands/src/db/migrate_v2.rs | 26 +++++++++++++++++++ .../src/providers/static_file/writer.rs | 24 +++++++++++------ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/crates/cli/commands/src/db/migrate_v2.rs b/crates/cli/commands/src/db/migrate_v2.rs index 61268f72032..66b1b1df4d7 100644 --- a/crates/cli/commands/src/db/migrate_v2.rs +++ b/crates/cli/commands/src/db/migrate_v2.rs @@ -135,6 +135,14 @@ impl Command { let mut writer = sf_provider.get_writer(first_block, StaticFileSegment::AccountChangeSets)?; + // The writer starts at the fixed range boundary (e.g. 2500000) which may be + // earlier than first_block (e.g. 2603897 from prune checkpoint). Pad with + // empty changesets for pruned blocks so the writer is aligned. + let writer_start = writer.next_block_number(); + for block in writer_start..first_block { + writer.append_account_changeset(vec![], block)?; + } + let mut count = 0u64; let mut walker = cursor.walk(Some(first_block))?.peekable(); @@ -177,6 +185,12 @@ impl Command { let mut writer = sf_provider.get_writer(first_block, StaticFileSegment::StorageChangeSets)?; + // Pad with empty changesets for pruned blocks (same as account changesets above). + let writer_start = writer.next_block_number(); + for block in writer_start..first_block { + writer.append_storage_changeset(vec![], block)?; + } + let mut count = 0u64; let mut walker = cursor.walk(Some(Default::default()))?.peekable(); @@ -238,6 +252,18 @@ impl Command { .map_or(0, |b| b + 1); let first_block = prune_start.max(existing.map_or(0, |b| b + 1)); + // Pad with empty blocks from the static file range boundary up to first_block + // so the writer is properly aligned (same as changeset migration). + { + let mut writer = + sf_provider.get_writer(first_block, StaticFileSegment::Receipts)?; + let writer_start = writer.next_block_number(); + for block in writer_start..first_block { + writer.increment_block(block)?; + } + writer.commit()?; + } + let block_range = first_block..=tip; let segment = reth_static_file::segments::Receipts; diff --git a/crates/storage/provider/src/providers/static_file/writer.rs b/crates/storage/provider/src/providers/static_file/writer.rs index 5a022bef1f0..809cd5554f9 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,19 @@ 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. From 47ea650557a23cd8760e1bc7a0094420844ba6df Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Fri, 24 Apr 2026 17:53:11 +0200 Subject: [PATCH 2/7] fmt --- crates/storage/provider/src/providers/static_file/writer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/storage/provider/src/providers/static_file/writer.rs b/crates/storage/provider/src/providers/static_file/writer.rs index 809cd5554f9..c4a9d4f1871 100644 --- a/crates/storage/provider/src/providers/static_file/writer.rs +++ b/crates/storage/provider/src/providers/static_file/writer.rs @@ -719,8 +719,7 @@ impl StaticFileProviderRW { 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)); + let prev_path = self.reader().directory().join(segment.filename(&prev_range)); prev_path.exists().then_some(prev_block) }); From 1e8981f6087c4076937b2619a7efa735574f9680 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Fri, 24 Apr 2026 17:55:00 +0200 Subject: [PATCH 3/7] better comments --- crates/cli/commands/src/db/migrate_v2.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/cli/commands/src/db/migrate_v2.rs b/crates/cli/commands/src/db/migrate_v2.rs index 66b1b1df4d7..68a50a40dfe 100644 --- a/crates/cli/commands/src/db/migrate_v2.rs +++ b/crates/cli/commands/src/db/migrate_v2.rs @@ -185,7 +185,9 @@ impl Command { let mut writer = sf_provider.get_writer(first_block, StaticFileSegment::StorageChangeSets)?; - // Pad with empty changesets for pruned blocks (same as account changesets above). + // The writer starts at the fixed range boundary (e.g. 2500000) which may be + // earlier than first_block (e.g. 2603897 from prune checkpoint). Pad with + // empty changesets for pruned blocks so the writer is aligned. let writer_start = writer.next_block_number(); for block in writer_start..first_block { writer.append_storage_changeset(vec![], block)?; @@ -252,11 +254,11 @@ impl Command { .map_or(0, |b| b + 1); let first_block = prune_start.max(existing.map_or(0, |b| b + 1)); - // Pad with empty blocks from the static file range boundary up to first_block - // so the writer is properly aligned (same as changeset migration). + // The writer starts at the fixed range boundary (e.g. 2500000) which may be + // earlier than first_block (e.g. 2603897 from prune checkpoint). Pad with + // empty blocks for pruned receipts so the writer is aligned. { - let mut writer = - sf_provider.get_writer(first_block, StaticFileSegment::Receipts)?; + let mut writer = sf_provider.get_writer(first_block, StaticFileSegment::Receipts)?; let writer_start = writer.next_block_number(); for block in writer_start..first_block { writer.increment_block(block)?; From 416c8c98228f3ae34adaae53fe8b274b6a203107 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Fri, 24 Apr 2026 18:00:42 +0200 Subject: [PATCH 4/7] tests --- .../src/providers/static_file/writer_tests.rs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) 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); + } } From 756d367d88b9c0309314695de804a9e8673de795 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Fri, 24 Apr 2026 21:43:27 +0200 Subject: [PATCH 5/7] use ensure_at_block --- crates/cli/commands/src/db/migrate_v2.rs | 42 +++++++++--------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/crates/cli/commands/src/db/migrate_v2.rs b/crates/cli/commands/src/db/migrate_v2.rs index 68a50a40dfe..485d2ba49c8 100644 --- a/crates/cli/commands/src/db/migrate_v2.rs +++ b/crates/cli/commands/src/db/migrate_v2.rs @@ -132,15 +132,11 @@ 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 starts at the fixed range boundary (e.g. 2500000) which may be - // earlier than first_block (e.g. 2603897 from prune checkpoint). Pad with - // empty changesets for pruned blocks so the writer is aligned. - let writer_start = writer.next_block_number(); - for block in writer_start..first_block { - writer.append_account_changeset(vec![], block)?; + // 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; @@ -182,15 +178,11 @@ 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 starts at the fixed range boundary (e.g. 2500000) which may be - // earlier than first_block (e.g. 2603897 from prune checkpoint). Pad with - // empty changesets for pruned blocks so the writer is aligned. - let writer_start = writer.next_block_number(); - for block in writer_start..first_block { - writer.append_storage_changeset(vec![], block)?; + // 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; @@ -254,15 +246,11 @@ impl Command { .map_or(0, |b| b + 1); let first_block = prune_start.max(existing.map_or(0, |b| b + 1)); - // The writer starts at the fixed range boundary (e.g. 2500000) which may be - // earlier than first_block (e.g. 2603897 from prune checkpoint). Pad with - // empty blocks for pruned receipts so the writer is aligned. - { - let mut writer = sf_provider.get_writer(first_block, StaticFileSegment::Receipts)?; - let writer_start = writer.next_block_number(); - for block in writer_start..first_block { - writer.increment_block(block)?; - } + // 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()?; } From 9404e1a31345f123931f2feb413df08351a6285e Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Sat, 25 Apr 2026 12:02:20 +0200 Subject: [PATCH 6/7] fix(db): start StorageChangeSets walker from first_block in migrate-v2 Previously the walker started from Default::default() (block 0, address 0x0), which produced count=0 when a StorageHistory prune checkpoint existed because the for loop started at first_block while the walker was stuck at block 0. --- crates/cli/commands/src/db/migrate_v2.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/cli/commands/src/db/migrate_v2.rs b/crates/cli/commands/src/db/migrate_v2.rs index 485d2ba49c8..4d957880bac 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}, @@ -186,7 +187,7 @@ impl Command { } 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(); From 1c27181692d2da6a4b006bdcac3d4590e18878c9 Mon Sep 17 00:00:00 2001 From: Alexey Shekhirin Date: Sat, 25 Apr 2026 12:22:50 +0200 Subject: [PATCH 7/7] fix(db): log receipt count in migrate-v2 --- crates/cli/commands/src/db/migrate_v2.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/cli/commands/src/db/migrate_v2.rs b/crates/cli/commands/src/db/migrate_v2.rs index 4d957880bac..072fe0d419e 100644 --- a/crates/cli/commands/src/db/migrate_v2.rs +++ b/crates/cli/commands/src/db/migrate_v2.rs @@ -255,6 +255,10 @@ impl Command { 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; @@ -262,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(()) }