diff --git a/crates/prune/prune/src/segments/mod.rs b/crates/prune/prune/src/segments/mod.rs index 5a09fd41c9..6909b90667 100644 --- a/crates/prune/prune/src/segments/mod.rs +++ b/crates/prune/prune/src/segments/mod.rs @@ -54,6 +54,38 @@ where }) } +/// Deletes ALL static file jars for a given segment. +/// +/// This is used for `PruneMode::Full` where all data should be removed, including the highest jar. +/// Unlike [`prune_static_files`], this does not preserve the most recent jar. +pub(crate) fn delete_static_files_segment( + provider: &Provider, + input: PruneInput, + segment: StaticFileSegment, +) -> Result +where + Provider: StaticFileProviderFactory, +{ + let deleted_headers = provider.static_file_provider().delete_segment(segment)?; + + if deleted_headers.is_empty() { + return Ok(SegmentOutput::done()) + } + + let tx_ranges = deleted_headers.iter().filter_map(|header| header.tx_range()); + + let pruned = tx_ranges.clone().map(|range| range.len()).sum::() as usize; + + Ok(SegmentOutput { + progress: PruneProgress::Finished, + pruned, + checkpoint: Some(SegmentOutputCheckpoint { + block_number: Some(input.to_block), + tx_number: tx_ranges.map(|range| range.end()).max(), + }), + }) +} + /// A segment represents a pruning of some portion of the data. /// /// Segments are called from [`Pruner`](crate::Pruner) with the following lifecycle: diff --git a/crates/prune/prune/src/segments/user/sender_recovery.rs b/crates/prune/prune/src/segments/user/sender_recovery.rs index beb6e85bf1..f175db4d95 100644 --- a/crates/prune/prune/src/segments/user/sender_recovery.rs +++ b/crates/prune/prune/src/segments/user/sender_recovery.rs @@ -49,6 +49,16 @@ where fn prune(&self, provider: &Provider, input: PruneInput) -> Result { if EitherWriterDestination::senders(provider).is_static_file() { debug!(target: "pruner", "Pruning transaction senders from static files."); + + if self.mode.is_full() { + debug!(target: "pruner", "PruneMode::Full: deleting all transaction senders static files."); + return segments::delete_static_files_segment( + provider, + input, + StaticFileSegment::TransactionSenders, + ) + } + return segments::prune_static_files( provider, input, diff --git a/crates/storage/provider/src/providers/static_file/manager.rs b/crates/storage/provider/src/providers/static_file/manager.rs index eeb3daad2b..8126363619 100644 --- a/crates/storage/provider/src/providers/static_file/manager.rs +++ b/crates/storage/provider/src/providers/static_file/manager.rs @@ -37,14 +37,15 @@ use reth_primitives_traits::{ AlloyBlockHeader as _, BlockBody as _, RecoveredBlock, SealedHeader, SignedTransaction, StorageEntry, }; +use reth_prune_types::PruneSegment; use reth_stages_types::PipelineTarget; use reth_static_file_types::{ find_fixed_range, HighestStaticFiles, SegmentHeader, SegmentRangeInclusive, StaticFileMap, StaticFileSegment, DEFAULT_BLOCKS_PER_STATIC_FILE, }; use reth_storage_api::{ - BlockBodyIndicesProvider, ChangeSetReader, DBProvider, StorageChangeSetReader, - StorageSettingsCache, + BlockBodyIndicesProvider, ChangeSetReader, DBProvider, PruneCheckpointReader, + StorageChangeSetReader, StorageSettingsCache, }; use reth_storage_errors::provider::{ProviderError, ProviderResult, StaticFileWriterError}; use std::{ @@ -985,6 +986,34 @@ impl StaticFileProvider { Ok(header) } + /// Deletes ALL static file jars for the given segment, including the highest one. + /// + /// CAUTION: destructive. Deletes all files on disk for this segment. + /// + /// This is used for `PruneMode::Full` where all data should be removed. + /// + /// Returns a list of `SegmentHeader`s from the deleted jars. + pub fn delete_segment(&self, segment: StaticFileSegment) -> ProviderResult> { + let mut deleted_headers = Vec::new(); + + while let Some(block_height) = self.get_highest_static_file_block(segment) { + debug!( + target: "provider::static_file", + ?segment, + ?block_height, + "Deleting static file jar" + ); + + let header = self.delete_jar(segment, block_height).inspect_err(|err| { + warn!(target: "provider::static_file", ?segment, %block_height, ?err, "Failed to delete static file jar") + })?; + + deleted_headers.push(header); + } + + Ok(deleted_headers) + } + /// Given a segment and block range it returns a cached /// [`StaticFileJarProvider`]. TODO(joshie): we should check the size and pop N if there's too /// many. @@ -1293,6 +1322,7 @@ impl StaticFileProvider { Provider: DBProvider + BlockReader + StageCheckpointReader + + PruneCheckpointReader + ChainSpecProvider + StorageSettingsCache, N: NodePrimitives, @@ -1452,7 +1482,7 @@ impl StaticFileProvider { /// database checkpoints or prune against them. pub fn check_file_consistency(&self, provider: &Provider) -> ProviderResult<()> where - Provider: DBProvider + ChainSpecProvider + StorageSettingsCache, + Provider: DBProvider + ChainSpecProvider + StorageSettingsCache + PruneCheckpointReader, { info!(target: "reth::cli", "Healing static file inconsistencies."); @@ -1469,7 +1499,7 @@ impl StaticFileProvider { provider: &'a Provider, ) -> impl Iterator + 'a where - Provider: DBProvider + ChainSpecProvider + StorageSettingsCache, + Provider: DBProvider + ChainSpecProvider + StorageSettingsCache + PruneCheckpointReader, { StaticFileSegment::iter() .filter(move |segment| self.should_check_segment(provider, *segment)) @@ -1481,7 +1511,7 @@ impl StaticFileProvider { segment: StaticFileSegment, ) -> bool where - Provider: DBProvider + ChainSpecProvider + StorageSettingsCache, + Provider: DBProvider + ChainSpecProvider + StorageSettingsCache + PruneCheckpointReader, { match segment { StaticFileSegment::Headers | StaticFileSegment::Transactions => true, @@ -1506,7 +1536,17 @@ impl StaticFileProvider { true } StaticFileSegment::TransactionSenders => { - !EitherWriterDestination::senders(provider).is_database() + if EitherWriterDestination::senders(provider).is_database() { + debug!(target: "reth::providers::static_file", ?segment, "Skipping senders segment: senders stored in database"); + return false; + } + + if Self::is_segment_fully_pruned(provider, PruneSegment::SenderRecovery) { + debug!(target: "reth::providers::static_file", ?segment, "Skipping senders segment: fully pruned"); + return false; + } + + true } StaticFileSegment::AccountChangeSets => { if EitherWriter::account_changesets_destination(provider).is_database() { @@ -1525,6 +1565,20 @@ impl StaticFileProvider { } } + /// Returns `true` if the given prune segment has a checkpoint with + /// [`reth_prune_types::PruneMode::Full`], indicating all data for this segment has been + /// intentionally deleted. + fn is_segment_fully_pruned(provider: &Provider, segment: PruneSegment) -> bool + where + Provider: PruneCheckpointReader, + { + provider + .get_prune_checkpoint(segment) + .ok() + .flatten() + .is_some_and(|checkpoint| checkpoint.prune_mode.is_full()) + } + /// Checks consistency of the latest static file segment and throws an error if at fault. /// Read-only. pub fn check_segment_consistency(&self, segment: StaticFileSegment) -> ProviderResult<()> {