diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs index 984ecd39c9..98bd25006f 100644 --- a/crates/config/src/config.rs +++ b/crates/config/src/config.rs @@ -348,6 +348,8 @@ impl Default for IndexHistoryConfig { pub struct PruneConfig { /// Minimum pruning interval measured in blocks. pub block_interval: usize, + /// The number of recent sidecars to keep in the static file provider. + pub recent_sidecars_kept_blocks: usize, /// Pruning configuration for every part of the data that can be pruned. #[serde(alias = "parts")] pub segments: PruneModes, @@ -355,7 +357,7 @@ pub struct PruneConfig { impl Default for PruneConfig { fn default() -> Self { - Self { block_interval: 5, segments: PruneModes::none() } + Self { block_interval: 5, recent_sidecars_kept_blocks: 0, segments: PruneModes::none() } } } diff --git a/crates/consensus/beacon/src/engine/test_utils.rs b/crates/consensus/beacon/src/engine/test_utils.rs index 8fd6f03d41..6a45102b14 100644 --- a/crates/consensus/beacon/src/engine/test_utils.rs +++ b/crates/consensus/beacon/src/engine/test_utils.rs @@ -410,6 +410,7 @@ where self.base_config.chain_spec.prune_delete_limit, None, watch::channel(FinishedExExHeight::NoExExs).1, + 0, ); let mut hooks = EngineHooks::new(); diff --git a/crates/engine/tree/src/persistence.rs b/crates/engine/tree/src/persistence.rs index b3e73ffcfd..934db6adcd 100644 --- a/crates/engine/tree/src/persistence.rs +++ b/crates/engine/tree/src/persistence.rs @@ -200,6 +200,7 @@ mod tests { 0, None, finished_exex_height_rx, + 0, ); PersistenceHandle::spawn_services(provider, pruner) diff --git a/crates/node/core/src/args/pruning.rs b/crates/node/core/src/args/pruning.rs index 1621f2d8ed..f2c8a6ec8b 100644 --- a/crates/node/core/src/args/pruning.rs +++ b/crates/node/core/src/args/pruning.rs @@ -23,6 +23,7 @@ impl PruningArgs { } Some(PruneConfig { block_interval: 5, + recent_sidecars_kept_blocks: 0, segments: PruneModes { sender_recovery: Some(PruneMode::Full), transaction_lookup: None, diff --git a/crates/prune/prune/src/builder.rs b/crates/prune/prune/src/builder.rs index af426a8986..bd8c95c225 100644 --- a/crates/prune/prune/src/builder.rs +++ b/crates/prune/prune/src/builder.rs @@ -21,6 +21,8 @@ pub struct PrunerBuilder { timeout: Option, /// The finished height of all `ExEx`'s. finished_exex_height: watch::Receiver, + /// The number of recent sidecars to keep in the static file provider. + recent_sidecars_kept_blocks: usize, } impl PrunerBuilder { @@ -32,6 +34,7 @@ impl PrunerBuilder { Self::default() .block_interval(pruner_config.block_interval) .segments(pruner_config.segments) + .recent_sidecars_kept_blocks(pruner_config.recent_sidecars_kept_blocks) } /// Sets the minimum pruning interval measured in blocks. @@ -70,6 +73,12 @@ impl PrunerBuilder { self } + /// Sets the number of recent sidecars to keep in the static file provider. + pub const fn recent_sidecars_kept_blocks(mut self, recent_sidecars_kept_blocks: usize) -> Self { + self.recent_sidecars_kept_blocks = recent_sidecars_kept_blocks; + self + } + /// Builds a [Pruner] from the current configuration with the given provider factory. pub fn build_with_provider_factory( self, @@ -87,6 +96,7 @@ impl PrunerBuilder { self.delete_limit, self.timeout, self.finished_exex_height, + self.recent_sidecars_kept_blocks, ) } @@ -100,6 +110,7 @@ impl PrunerBuilder { self.delete_limit, self.timeout, self.finished_exex_height, + self.recent_sidecars_kept_blocks, ) } } @@ -112,6 +123,8 @@ impl Default for PrunerBuilder { delete_limit: MAINNET.prune_delete_limit, timeout: None, finished_exex_height: watch::channel(FinishedExExHeight::NoExExs).1, + recent_sidecars_kept_blocks: 0, /* not enabled by default + * recent_sidecars_kept_blocks: 518400, // 18 days */ } } } diff --git a/crates/prune/prune/src/metrics.rs b/crates/prune/prune/src/metrics.rs index 246dc317b0..78d7eca6ef 100644 --- a/crates/prune/prune/src/metrics.rs +++ b/crates/prune/prune/src/metrics.rs @@ -10,6 +10,8 @@ use std::collections::HashMap; pub(crate) struct Metrics { /// Pruning duration pub(crate) duration_seconds: Histogram, + /// The height of the oldest sidecars + pub(crate) oldest_sidecars_height: Gauge, #[metric(skip)] prune_segments: HashMap, } diff --git a/crates/prune/prune/src/pruner.rs b/crates/prune/prune/src/pruner.rs index dea50406a5..387dbc47e1 100644 --- a/crates/prune/prune/src/pruner.rs +++ b/crates/prune/prune/src/pruner.rs @@ -9,8 +9,13 @@ use reth_db_api::database::Database; use reth_exex_types::FinishedExExHeight; use reth_provider::{DatabaseProviderRW, ProviderFactory, PruneCheckpointReader}; use reth_prune_types::{PruneLimiter, PruneProgress, PruneSegment, PrunerOutput}; +use reth_static_file_types::{find_fixed_range, StaticFileSegment}; use reth_tokio_util::{EventSender, EventStream}; -use std::time::{Duration, Instant}; +use std::{ + fs, + path::Path, + time::{Duration, Instant}, +}; use tokio::sync::watch; use tracing::debug; @@ -41,6 +46,8 @@ pub struct Pruner { timeout: Option, /// The finished height of all `ExEx`'s. finished_exex_height: watch::Receiver, + /// The number of recent sidecars to keep in the static file provider. + recent_sidecars_kept_blocks: usize, #[doc(hidden)] metrics: Metrics, event_sender: EventSender, @@ -54,6 +61,7 @@ impl Pruner { delete_limit: usize, timeout: Option, finished_exex_height: watch::Receiver, + recent_sidecars_kept_blocks: usize, ) -> Self { Self { provider_factory: (), @@ -63,6 +71,7 @@ impl Pruner { delete_limit, timeout, finished_exex_height, + recent_sidecars_kept_blocks, metrics: Metrics::default(), event_sender: Default::default(), } @@ -71,6 +80,7 @@ impl Pruner { impl Pruner> { /// Crates a new pruner with the given provider factory. + #[allow(clippy::too_many_arguments)] pub fn new( provider_factory: ProviderFactory, segments: Vec>>, @@ -78,6 +88,7 @@ impl Pruner> { delete_limit: usize, timeout: Option, finished_exex_height: watch::Receiver, + recent_sidecars_kept_blocks: usize, ) -> Self { Self { provider_factory, @@ -87,6 +98,7 @@ impl Pruner> { delete_limit, timeout, finished_exex_height, + recent_sidecars_kept_blocks, metrics: Metrics::default(), event_sender: Default::default(), } @@ -129,6 +141,8 @@ impl Pruner { let (stats, deleted_entries, output) = self.prune_segments(provider, tip_block_number, &mut limiter)?; + self.prune_ancient_sidecars(provider, tip_block_number); + self.previous_tip_block_number = Some(tip_block_number); let elapsed = start.elapsed(); @@ -294,6 +308,80 @@ impl Pruner { } } } + + /// Prunes ancient sidecars data from the static file provider. + pub fn prune_ancient_sidecars( + &mut self, + provider: &DatabaseProviderRW, + tip_block_number: BlockNumber, + ) { + if self.recent_sidecars_kept_blocks == 0 { + return + } + + let static_file_provider = provider.static_file_provider(); + + let prune_target_block = + tip_block_number.saturating_sub(self.recent_sidecars_kept_blocks as u64); + let mut range_start = find_fixed_range(prune_target_block).start(); + + if range_start == 0 { + return + } + + debug!( + target: "pruner", + %tip_block_number, + "Ancient sidecars pruning started", + ); + + while range_start > 0 { + let range = find_fixed_range(range_start - 1); + let path = + static_file_provider.path().join(StaticFileSegment::Sidecars.filename(&range)); + + if path.exists() { + delete_static_files(&path); + self.metrics.oldest_sidecars_height.set(range.end() as f64 + 1_f64); + } else { + debug!(target: "pruner", path = %path.display(), "Static file not found, skipping"); + break + } + + range_start = range.start(); + } + + debug!( + target: "pruner", + %tip_block_number, + "Ancient sidecars pruning finished", + ); + } +} + +fn delete_static_files(path: &Path) { + // Delete the main file + if let Err(err) = fs::remove_file(path) { + debug!(target: "pruner", path = %path.display(), %err, "Failed to remove file"); + } else { + debug!(target: "pruner", path = %path.display(), "Removed file"); + } + + // Delete the .conf file + let conf_path = path.with_extension("conf"); + if let Err(err) = fs::remove_file(&conf_path) { + debug!(target: "pruner", path = %conf_path.display(), %err, "Failed to remove .conf file"); + } else { + debug!(target: "pruner", path = %conf_path.display(), "Removed .conf file"); + } + + // Delete the .off file + let off_path = path.with_extension("off"); + if let Err(err) = fs::remove_file(&off_path) { + debug!(target: "pruner", path = %off_path.display(), %err, "Failed to remove .off file"); + } else { + debug!(target: "pruner", path = %off_path.display(), "Removed .off file"); + } } impl Pruner { @@ -347,6 +435,7 @@ mod tests { 0, None, finished_exex_height_rx, + 0, ); // No last pruned block number was set before diff --git a/crates/prune/prune/src/segments/mod.rs b/crates/prune/prune/src/segments/mod.rs index d9f4538065..e04c9ac518 100644 --- a/crates/prune/prune/src/segments/mod.rs +++ b/crates/prune/prune/src/segments/mod.rs @@ -14,7 +14,7 @@ use reth_prune_types::{ }; pub use set::SegmentSet; pub use static_file::{ - Headers as StaticFileHeaders, Receipts as StaticFileReceipts, + Headers as StaticFileHeaders, Receipts as StaticFileReceipts, Sidecars as StaticFileSidecars, Transactions as StaticFileTransactions, }; use std::{fmt::Debug, ops::RangeInclusive}; diff --git a/crates/prune/prune/src/segments/set.rs b/crates/prune/prune/src/segments/set.rs index d152bfe262..29cfaeac68 100644 --- a/crates/prune/prune/src/segments/set.rs +++ b/crates/prune/prune/src/segments/set.rs @@ -1,6 +1,6 @@ use crate::segments::{ - AccountHistory, ReceiptsByLogs, Segment, SenderRecovery, StorageHistory, TransactionLookup, - UserReceipts, + AccountHistory, ReceiptsByLogs, Segment, SenderRecovery, StaticFileSidecars, StorageHistory, + TransactionLookup, UserReceipts, }; use reth_db_api::database::Database; use reth_provider::providers::StaticFileProvider; @@ -60,7 +60,9 @@ impl SegmentSet { // Static file transactions .segment(StaticFileTransactions::new(static_file_provider.clone())) // Static file receipts - .segment(StaticFileReceipts::new(static_file_provider)) + .segment(StaticFileReceipts::new(static_file_provider.clone())) + // Static file receipts + .segment(StaticFileSidecars::new(static_file_provider)) // Account history .segment_opt(account_history.map(AccountHistory::new)) // Storage history diff --git a/crates/prune/prune/src/segments/static_file/mod.rs b/crates/prune/prune/src/segments/static_file/mod.rs index cb9dc79c6c..411742dc9e 100644 --- a/crates/prune/prune/src/segments/static_file/mod.rs +++ b/crates/prune/prune/src/segments/static_file/mod.rs @@ -1,7 +1,9 @@ mod headers; mod receipts; +mod sidecars; mod transactions; pub use headers::Headers; pub use receipts::Receipts; +pub use sidecars::Sidecars; pub use transactions::Transactions; diff --git a/crates/prune/prune/src/segments/static_file/sidecars.rs b/crates/prune/prune/src/segments/static_file/sidecars.rs new file mode 100644 index 0000000000..7760e059fd --- /dev/null +++ b/crates/prune/prune/src/segments/static_file/sidecars.rs @@ -0,0 +1,184 @@ +use crate::{ + segments::{PruneInput, Segment}, + PrunerError, +}; +use reth_db::tables; +use reth_db_api::database::Database; +use reth_provider::{providers::StaticFileProvider, DatabaseProviderRW}; +use reth_prune_types::{ + PruneMode, PruneProgress, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint, +}; +use reth_static_file_types::StaticFileSegment; +use tracing::{instrument, trace}; + +#[derive(Debug)] +pub struct Sidecars { + static_file_provider: StaticFileProvider, +} + +impl Sidecars { + pub const fn new(static_file_provider: StaticFileProvider) -> Self { + Self { static_file_provider } + } +} + +impl Segment for Sidecars { + fn segment(&self) -> PruneSegment { + PruneSegment::Sidecars + } + + fn mode(&self) -> Option { + self.static_file_provider + .get_highest_static_file_block(StaticFileSegment::Sidecars) + .map(PruneMode::before_inclusive) + } + + fn purpose(&self) -> PrunePurpose { + PrunePurpose::StaticFile + } + + #[instrument(level = "trace", target = "pruner", skip(self, provider), ret)] + fn prune( + &self, + provider: &DatabaseProviderRW, + input: PruneInput, + ) -> Result { + let (block_range_start, block_range_end) = match input.get_next_block_range() { + Some(range) => (*range.start(), *range.end()), + None => { + trace!(target: "pruner", "No sidecars to prune"); + return Ok(SegmentOutput::done()) + } + }; + let last_pruned_block = + if block_range_start == 0 { None } else { Some(block_range_start - 1) }; + let range = last_pruned_block.map_or(0, |block| block + 1)..=block_range_end; + + let mut limiter = input.limiter; + + let mut last_pruned_block: Option = None; + let (pruned, done) = provider.prune_table_with_range::( + range, + &mut limiter, + |_| false, + |row| last_pruned_block = Some(row.0), + )?; + trace!(target: "pruner", %pruned, %done, "Pruned sidecars"); + + let done = last_pruned_block.map_or(false, |block| block == block_range_end); + let progress = PruneProgress::new(done, &limiter); + + Ok(SegmentOutput { + progress, + pruned, + checkpoint: Some(SegmentOutputCheckpoint { + block_number: last_pruned_block, + tx_number: None, + }), + }) + } +} + +#[cfg(test)] +mod tests { + use crate::segments::{PruneInput, Segment, SegmentOutput}; + use alloy_primitives::{BlockNumber, B256}; + use assert_matches::assert_matches; + use reth_db::tables; + use reth_provider::{PruneCheckpointReader, StaticFileProviderFactory}; + use reth_prune_types::{ + PruneCheckpoint, PruneInterruptReason, PruneLimiter, PruneMode, PruneProgress, PruneSegment, + }; + use reth_stages::test_utils::{StorageKind, TestStageDB}; + use reth_testing_utils::{generators, generators::random_block_range}; + use tracing::trace; + + #[test] + fn prune() { + reth_tracing::init_test_tracing(); + + let db = TestStageDB::default(); + let mut rng = generators::rng(); + + let blocks = random_block_range(&mut rng, 1..=15, B256::ZERO, 0..1); + db.insert_blocks(blocks.iter(), StorageKind::Database(None)).expect("insert blocks"); + + assert_eq!(db.table::().unwrap().len(), blocks.len()); + + let test_prune = |to_block: BlockNumber, expected_result: (PruneProgress, usize)| { + let segment = super::Sidecars::new(db.factory.static_file_provider()); + let prune_mode = PruneMode::Before(to_block); + let mut limiter = PruneLimiter::default().set_deleted_entries_limit(10); + let input = PruneInput { + previous_checkpoint: db + .factory + .provider() + .unwrap() + .get_prune_checkpoint(PruneSegment::Sidecars) + .unwrap(), + to_block, + limiter: limiter.clone(), + }; + + let next_block_number_to_prune = db + .factory + .provider() + .unwrap() + .get_prune_checkpoint(PruneSegment::Sidecars) + .unwrap() + .and_then(|checkpoint| checkpoint.block_number) + .map(|block_number| block_number + 1) + .unwrap_or_default(); + + let provider = db.factory.provider_rw().unwrap(); + let result = segment.prune(&provider, input.clone()).unwrap(); + limiter.increment_deleted_entries_count_by(result.pruned); + trace!(target: "pruner::test", + expected_prune_progress=?expected_result.0, + expected_pruned=?expected_result.1, + result=?result, + "SegmentOutput" + ); + + assert_matches!( + result, + SegmentOutput {progress, pruned, checkpoint: Some(_)} + if (progress, pruned) == expected_result + ); + segment + .save_checkpoint( + &provider, + result.checkpoint.unwrap().as_prune_checkpoint(prune_mode), + ) + .unwrap(); + provider.commit().expect("commit"); + + let last_pruned_block_number = to_block.min( + next_block_number_to_prune + input.limiter.deleted_entries_limit().unwrap() as u64, + ); + + assert_eq!( + db.table::().unwrap().len(), + blocks.len() - last_pruned_block_number as usize + ); + assert_eq!( + db.factory + .provider() + .unwrap() + .get_prune_checkpoint(PruneSegment::Sidecars) + .unwrap(), + Some(PruneCheckpoint { + block_number: Some(last_pruned_block_number), + tx_number: None, + prune_mode + }) + ); + }; + + test_prune( + 12, + (PruneProgress::HasMoreData(PruneInterruptReason::DeletedEntriesLimitReached), 10), + ); + test_prune(12, (PruneProgress::Finished, 2)); + } +} diff --git a/crates/prune/types/src/segment.rs b/crates/prune/types/src/segment.rs index d9b678292e..35695f154b 100644 --- a/crates/prune/types/src/segment.rs +++ b/crates/prune/types/src/segment.rs @@ -27,15 +27,19 @@ pub enum PruneSegment { Headers, /// Prune segment responsible for the `Transactions` table. Transactions, + /// Prune segment responsible for the `Sidecars` table. + Sidecars, } impl PruneSegment { /// Returns minimum number of blocks to left in the database for this segment. pub const fn min_blocks(&self, purpose: PrunePurpose) -> u64 { match self { - Self::SenderRecovery | Self::TransactionLookup | Self::Headers | Self::Transactions => { - 0 - } + Self::SenderRecovery | + Self::TransactionLookup | + Self::Headers | + Self::Transactions | + Self::Sidecars => 0, Self::Receipts if purpose.is_static_file() => 0, Self::ContractLogs | Self::AccountHistory | Self::StorageHistory => { MINIMUM_PRUNING_DISTANCE diff --git a/crates/storage/provider/src/providers/static_file/manager.rs b/crates/storage/provider/src/providers/static_file/manager.rs index c2e23e9147..ab2f54c859 100644 --- a/crates/storage/provider/src/providers/static_file/manager.rs +++ b/crates/storage/provider/src/providers/static_file/manager.rs @@ -1031,7 +1031,7 @@ impl StaticFileProvider { Ok(data) } - #[cfg(any(test, feature = "test-utils"))] + // #[cfg(any(test, feature = "test-utils"))] /// Returns `static_files` directory pub fn path(&self) -> &Path { &self.path