diff --git a/Cargo.lock b/Cargo.lock index c22d374498d..b9d16a3209f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9118,6 +9118,7 @@ dependencies = [ "serde", "shellexpand", "strum 0.27.2", + "thiserror 2.0.17", "tokio", "toml", "tracing", @@ -9946,6 +9947,7 @@ dependencies = [ "proptest", "proptest-arbitrary-interop", "reth-codecs", + "reth-tracing", "serde", "serde_json", "strum 0.27.2", diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs index dd2e7046b0c..f51bc4afd61 100644 --- a/crates/config/src/config.rs +++ b/crates/config/src/config.rs @@ -33,7 +33,7 @@ pub struct Config { impl Config { /// Sets the pruning configuration. - pub const fn set_prune_config(&mut self, prune_config: PruneConfig) { + pub fn set_prune_config(&mut self, prune_config: PruneConfig) { self.prune = prune_config; } } @@ -458,7 +458,6 @@ impl PruneConfig { /// Merges another `PruneConfig` into this one, taking values from the other config if and only /// if the corresponding value in this config is not set. pub fn merge(&mut self, other: Self) { - #[expect(deprecated)] let Self { block_interval, segments: @@ -470,7 +469,7 @@ impl PruneConfig { storage_history, bodies_history, merkle_changesets, - receipts_log_filter: (), + receipts_log_filter, }, } = other; @@ -488,6 +487,9 @@ impl PruneConfig { self.segments.bodies_history = self.segments.bodies_history.or(bodies_history); // Merkle changesets is not optional, so we just replace it if provided self.segments.merkle_changesets = merkle_changesets; + if self.segments.receipts_log_filter.0.is_empty() && !receipts_log_filter.0.is_empty() { + self.segments.receipts_log_filter = receipts_log_filter; + } } } @@ -514,9 +516,10 @@ where mod tests { use super::{Config, EXTENSION}; use crate::PruneConfig; + use alloy_primitives::Address; use reth_network_peers::TrustedPeer; use reth_prune_types::{PruneMode, PruneModes}; - use std::{path::Path, str::FromStr, time::Duration}; + use std::{collections::BTreeMap, path::Path, str::FromStr, time::Duration}; fn with_tempdir(filename: &str, proc: fn(&std::path::Path)) { let temp_dir = tempfile::tempdir().unwrap(); @@ -1005,8 +1008,7 @@ receipts = 'full' storage_history: Some(PruneMode::Before(5000)), bodies_history: None, merkle_changesets: PruneMode::Before(0), - #[expect(deprecated)] - receipts_log_filter: (), + receipts_log_filter: BTreeMap::from([(Address::random(), PruneMode::Full)]).into(), }, }; @@ -1020,11 +1022,15 @@ receipts = 'full' storage_history: Some(PruneMode::Distance(3000)), bodies_history: None, merkle_changesets: PruneMode::Distance(10000), - #[expect(deprecated)] - receipts_log_filter: (), + receipts_log_filter: BTreeMap::from([ + (Address::random(), PruneMode::Distance(1000)), + (Address::random(), PruneMode::Before(2000)), + ]) + .into(), }, }; + let original_receipts_log_filter = config1.segments.receipts_log_filter.clone(); config1.merge(config2); // Check that the configuration has been merged. Any configuration present in config1 @@ -1036,6 +1042,7 @@ receipts = 'full' assert_eq!(config1.segments.account_history, Some(PruneMode::Distance(2000))); assert_eq!(config1.segments.storage_history, Some(PruneMode::Before(5000))); assert_eq!(config1.segments.merkle_changesets, PruneMode::Distance(10000)); + assert_eq!(config1.segments.receipts_log_filter, original_receipts_log_filter); } #[test] diff --git a/crates/exex/exex/src/backfill/factory.rs b/crates/exex/exex/src/backfill/factory.rs index 29734b905e2..d9a51bc47a7 100644 --- a/crates/exex/exex/src/backfill/factory.rs +++ b/crates/exex/exex/src/backfill/factory.rs @@ -39,7 +39,7 @@ impl BackfillJobFactory { } /// Sets the prune modes - pub const fn with_prune_modes(mut self, prune_modes: PruneModes) -> Self { + pub fn with_prune_modes(mut self, prune_modes: PruneModes) -> Self { self.prune_modes = prune_modes; self } diff --git a/crates/node/builder/src/launch/common.rs b/crates/node/builder/src/launch/common.rs index 1f5d5dff83b..8722cd92b15 100644 --- a/crates/node/builder/src/launch/common.rs +++ b/crates/node/builder/src/launch/common.rs @@ -1172,7 +1172,6 @@ mod tests { storage_history_before: None, bodies_pre_merge: false, bodies_distance: None, - #[expect(deprecated)] receipts_log_filter: None, bodies_before: None, }, diff --git a/crates/node/core/Cargo.toml b/crates/node/core/Cargo.toml index e2852e01a81..c6981acd963 100644 --- a/crates/node/core/Cargo.toml +++ b/crates/node/core/Cargo.toml @@ -44,14 +44,15 @@ alloy-consensus.workspace = true alloy-eips.workspace = true # misc -eyre.workspace = true clap = { workspace = true, features = ["derive", "env"] } +derive_more.workspace = true +eyre.workspace = true humantime.workspace = true rand.workspace = true -derive_more.workspace = true -toml.workspace = true serde.workspace = true strum = { workspace = true, features = ["derive"] } +thiserror.workspace = true +toml.workspace = true url.workspace = true # io diff --git a/crates/node/core/src/args/pruning.rs b/crates/node/core/src/args/pruning.rs index 0bd72e207ea..d6605e6fd4f 100644 --- a/crates/node/core/src/args/pruning.rs +++ b/crates/node/core/src/args/pruning.rs @@ -1,13 +1,15 @@ //! Pruning and full node arguments -use std::ops::Not; +use std::{num::ParseIntError, ops::Not}; use crate::primitives::EthereumHardfork; -use alloy_primitives::BlockNumber; +use alloy_primitives::{Address, BlockNumber}; use clap::{builder::RangedU64ValueParser, Args}; use reth_chainspec::EthereumHardforks; use reth_config::config::PruneConfig; -use reth_prune_types::{PruneMode, PruneModes, MINIMUM_PRUNING_DISTANCE}; +use reth_prune_types::{ + PruneMode, PruneModes, PruneSegmentError, ReceiptsLogPruneConfig, MINIMUM_PRUNING_DISTANCE, +}; /// Parameters for pruning and full node #[derive(Debug, Clone, Args, PartialEq, Eq, Default)] @@ -61,14 +63,8 @@ pub struct PruningArgs { #[arg(long = "prune.receipts.before", value_name = "BLOCK_NUMBER", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance"])] pub receipts_before: Option, /// Receipts Log Filter - #[arg( - long = "prune.receipts-log-filter", - alias = "prune.receiptslogfilter", - value_name = "FILTER_CONFIG", - hide = true - )] - #[deprecated] - pub receipts_log_filter: Option, + #[arg(long = "prune.receiptslogfilter", value_name = "FILTER_CONFIG", conflicts_with_all = &["receipts_full", "receipts_pre_merge", "receipts_distance", "receipts_before"], value_parser = parse_receipts_log_filter)] + pub receipts_log_filter: Option, // Account History /// Prunes all account history. @@ -136,8 +132,7 @@ impl PruningArgs { .block_number() .map(PruneMode::Before), merkle_changesets: PruneMode::Distance(MINIMUM_PRUNING_DISTANCE), - #[expect(deprecated)] - receipts_log_filter: (), + receipts_log_filter: ReceiptsLogPruneConfig::new(), }, } } @@ -164,14 +159,13 @@ impl PruningArgs { if let Some(mode) = self.storage_history_prune_mode() { config.segments.storage_history = Some(mode); } - - // Log warning if receipts_log_filter is set (deprecated feature) - #[expect(deprecated)] - if self.receipts_log_filter.is_some() { - tracing::warn!( - target: "reth::cli", - "The --prune.receiptslogfilter flag is deprecated and has no effect. It will be removed in a future release." - ); + if let Some(receipt_logs) = + self.receipts_log_filter.as_ref().filter(|c| !c.is_empty()).cloned() + { + config.segments.receipts_log_filter = receipt_logs; + // need to remove the receipts segment filter entirely because that takes precedence + // over the logs filter + config.segments.receipts.take(); } config.is_default().not().then_some(config) @@ -259,3 +253,173 @@ impl PruningArgs { } } } + +/// Error while parsing a `[ReceiptsLogPruneConfig`] +#[derive(thiserror::Error, Debug)] +pub(crate) enum ReceiptsLogError { + /// The format of the filter is invalid. + #[error("invalid filter format: {0}")] + InvalidFilterFormat(String), + /// Address is invalid. + #[error("address is invalid: {0}")] + InvalidAddress(String), + /// The prune mode is not one of full, distance, before. + #[error("prune mode is invalid: {0}")] + InvalidPruneMode(String), + /// The distance value supplied is invalid. + #[error("distance is invalid: {0}")] + InvalidDistance(ParseIntError), + /// The block number supplied is invalid. + #[error("block number is invalid: {0}")] + InvalidBlockNumber(ParseIntError), + #[error(transparent)] + PruneSegment(#[from] PruneSegmentError), +} + +/// Parses `,` separated pruning info into [`ReceiptsLogPruneConfig`]. +pub(crate) fn parse_receipts_log_filter( + value: &str, +) -> Result { + let mut config = ReceiptsLogPruneConfig::new(); + // Split out each of the filters. + let filters = value.split(','); + for filter in filters { + let parts: Vec<&str> = filter.split(':').collect(); + if parts.len() < 2 { + return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string())); + } + // Parse the address + let address = parts[0] + .parse::
() + .map_err(|_| ReceiptsLogError::InvalidAddress(parts[0].to_string()))?; + + // Parse the prune mode + let prune_mode = match parts[1] { + "full" => PruneMode::Full, + s if s.starts_with("distance") => { + if parts.len() < 3 { + return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string())); + } + let distance = + parts[2].parse::().map_err(ReceiptsLogError::InvalidDistance)?; + PruneMode::Distance(distance) + } + s if s.starts_with("before") => { + if parts.len() < 3 { + return Err(ReceiptsLogError::InvalidFilterFormat(filter.to_string())); + } + let block_number = + parts[2].parse::().map_err(ReceiptsLogError::InvalidBlockNumber)?; + PruneMode::Before(block_number) + } + _ => return Err(ReceiptsLogError::InvalidPruneMode(parts[1].to_string())), + }; + config.insert(address, prune_mode); + } + + let errors = config.validate_and_fix(); + for error in errors { + reth_tracing::tracing::warn!("Receipt log pruning CLI arguments error: {}", error); + } + + Ok(config) +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::*; + use alloy_primitives::address; + use clap::Parser; + + /// A helper type to parse Args more easily + #[derive(Parser)] + struct CommandParser { + #[command(flatten)] + args: T, + } + + #[test] + fn pruning_args_sanity_check() { + let args = CommandParser::::parse_from([ + "reth", + "--prune.receiptslogfilter", + "0x0000000000000000000000000000000000000003:before:5000000", + ]) + .args; + let mut config = ReceiptsLogPruneConfig::default(); + config.insert( + address!("0x0000000000000000000000000000000000000003"), + PruneMode::Before(5000000), + ); + assert_eq!(args.receipts_log_filter, Some(config)); + } + + #[test] + fn parse_receiptslogfilter() { + let default_args = PruningArgs::default(); + let args = CommandParser::::parse_from(["reth"]).args; + assert_eq!(args, default_args); + } + + #[test] + fn test_parse_receipts_log_filter() { + let addr1 = address!("0x0000000000000000000000000000000000000001"); + let addr2 = address!("0x0000000000000000000000000000000000000002"); + let addr3 = address!("0x0000000000000000000000000000000000000003"); + + let filters = [ + format!("{addr1}:full"), + format!("{addr2}:distance:1000"), + format!("{addr3}:before:5000000"), + ] + .join(","); + + // Args can be parsed. + let result = parse_receipts_log_filter(&filters); + assert!(result.is_ok()); + let config = result.unwrap(); + + // Check that the args were parsed correctly. + assert_eq!( + config, + BTreeMap::from([(addr1, PruneMode::Full), (addr3, PruneMode::Before(5000000))]).into() + ); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_filter_format() { + let result = parse_receipts_log_filter("invalid_format"); + assert!(matches!(result, Err(ReceiptsLogError::InvalidFilterFormat(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_address() { + let result = parse_receipts_log_filter("invalid_address:full"); + assert!(matches!(result, Err(ReceiptsLogError::InvalidAddress(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_prune_mode() { + let result = + parse_receipts_log_filter("0x0000000000000000000000000000000000000000:invalid_mode"); + assert!(matches!(result, Err(ReceiptsLogError::InvalidPruneMode(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_distance() { + let result = parse_receipts_log_filter( + "0x0000000000000000000000000000000000000000:distance:invalid_distance", + ); + assert!(matches!(result, Err(ReceiptsLogError::InvalidDistance(_)))); + } + + #[test] + fn test_parse_receipts_log_filter_invalid_block_number() { + let result = parse_receipts_log_filter( + "0x0000000000000000000000000000000000000000:before:invalid_block", + ); + assert!(matches!(result, Err(ReceiptsLogError::InvalidBlockNumber(_)))); + } +} diff --git a/crates/prune/prune/src/builder.rs b/crates/prune/prune/src/builder.rs index f61aa6bd46d..78283710e15 100644 --- a/crates/prune/prune/src/builder.rs +++ b/crates/prune/prune/src/builder.rs @@ -43,7 +43,7 @@ impl PrunerBuilder { } /// Sets the configuration for every part of the data that can be pruned. - pub const fn segments(mut self, segments: PruneModes) -> Self { + pub fn segments(mut self, segments: PruneModes) -> Self { self.segments = segments; self } diff --git a/crates/prune/prune/src/segments/set.rs b/crates/prune/prune/src/segments/set.rs index acd71f52e1b..fbda033a025 100644 --- a/crates/prune/prune/src/segments/set.rs +++ b/crates/prune/prune/src/segments/set.rs @@ -59,7 +59,6 @@ where _static_file_provider: StaticFileProvider, prune_modes: PruneModes, ) -> Self { - #[expect(deprecated)] let PruneModes { sender_recovery, transaction_lookup, @@ -68,7 +67,7 @@ where storage_history, bodies_history, merkle_changesets, - receipts_log_filter: (), + receipts_log_filter: _, } = prune_modes; Self::default() diff --git a/crates/prune/types/Cargo.toml b/crates/prune/types/Cargo.toml index 30adbb14d91..f5a957c1d3c 100644 --- a/crates/prune/types/Cargo.toml +++ b/crates/prune/types/Cargo.toml @@ -13,6 +13,7 @@ workspace = true [dependencies] reth-codecs = { workspace = true, optional = true } +reth-tracing = { workspace = true, optional = true } alloy-primitives.workspace = true derive_more.workspace = true @@ -44,6 +45,7 @@ std = [ "serde_json/std", "thiserror/std", "strum/std", + "dep:reth-tracing", ] test-utils = [ "std", diff --git a/crates/prune/types/src/lib.rs b/crates/prune/types/src/lib.rs index a588693892a..fa4cbc87fbb 100644 --- a/crates/prune/types/src/lib.rs +++ b/crates/prune/types/src/lib.rs @@ -12,18 +12,113 @@ extern crate alloc; mod checkpoint; -mod event; -mod mode; -mod pruner; -mod segment; -mod target; - pub use checkpoint::PruneCheckpoint; +mod event; pub use event::PrunerEvent; +mod mode; pub use mode::PruneMode; +mod pruner; pub use pruner::{ PruneInterruptReason, PruneProgress, PrunedSegmentInfo, PrunerOutput, SegmentOutput, SegmentOutputCheckpoint, }; +mod segment; pub use segment::{PrunePurpose, PruneSegment, PruneSegmentError}; +mod target; pub use target::{PruneModes, UnwindTargetPrunedError, MINIMUM_PRUNING_DISTANCE}; + +use alloc::{collections::BTreeMap, vec::Vec}; +use alloy_primitives::{Address, BlockNumber}; +use derive_more::{Deref, DerefMut, From}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer}; + +/// Configuration for pruning receipts not associated with logs emitted by the specified contracts. +#[derive(Debug, Clone, PartialEq, Eq, Default, Deref, DerefMut, From)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] +pub struct ReceiptsLogPruneConfig(pub BTreeMap); + +impl ReceiptsLogPruneConfig { + /// Creates an empty config. + pub const fn new() -> Self { + Self(BTreeMap::new()) + } + + /// Returns `true` if the config is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Validates the configuration and fixes any issues if possible. + pub fn validate_and_fix(&mut self) -> Vec { + let mut errors = Vec::new(); + self.retain(|address, mode| { + if mode.is_distance() { + errors.push(PruneSegmentError::UnsupportedReceiptsLogFilterDistance(*address)); + return false; + } + + true + }); + errors + } + + /// Given the `tip` block number, consolidates the structure so it can easily be queried for + /// filtering across a range of blocks. + /// + /// Example: + /// + /// `{ addrA: Before(872), addrB: Before(500), addrC: Distance(128) }` + /// + /// for `tip: 1000`, gets transformed to a map such as: + /// + /// `{ 500: [addrB], 872: [addrA, addrC] }` + /// + /// The [`BlockNumber`] key of the new map should be viewed as `PruneMode::Before(block)`, which + /// makes the previous result equivalent to + /// + /// `{ Before(500): [addrB], Before(872): [addrA, addrC] }` + pub fn group_by_block( + &self, + tip: BlockNumber, + pruned_block: Option, + ) -> Result>, PruneSegmentError> { + let mut map = BTreeMap::new(); + let base_block = pruned_block.unwrap_or_default() + 1; + + for (address, mode) in &self.0 { + // Getting `None`, means that there is nothing to prune yet, so we need it to include in + // the BTreeMap (block = 0), otherwise it will be excluded. + // Reminder that this BTreeMap works as an inclusion list that excludes (prunes) all + // other receipts. + // + // Reminder, that we increment because the [`BlockNumber`] key of the new map should be + // viewed as `PruneMode::Before(block)` + let block = base_block.max( + mode.prune_target_block(tip, PruneSegment::ContractLogs, PrunePurpose::User)? + .map(|(block, _)| block) + .unwrap_or_default() + + 1, + ); + + map.entry(block).or_insert_with(Vec::new).push(*address) + } + Ok(map) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for ReceiptsLogPruneConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut config = Self(BTreeMap::deserialize(deserializer)?); + let errors = config.validate_and_fix(); + #[cfg(feature = "std")] + for error in errors { + reth_tracing::tracing::warn!("Receipt log pruning config error: {}", error); + } + Ok(config) + } +} diff --git a/crates/prune/types/src/segment.rs b/crates/prune/types/src/segment.rs index 36e39fcb585..d9fcb61088c 100644 --- a/crates/prune/types/src/segment.rs +++ b/crates/prune/types/src/segment.rs @@ -1,6 +1,7 @@ #![allow(deprecated)] // necessary to all defining deprecated `PruneSegment` variants use crate::MINIMUM_PRUNING_DISTANCE; +use alloy_primitives::Address; use derive_more::Display; use strum::{EnumIter, IntoEnumIterator}; use thiserror::Error; @@ -115,6 +116,9 @@ pub enum PruneSegmentError { /// Invalid configuration of a prune segment. #[error("the configuration provided for {0} is invalid")] Configuration(PruneSegment), + /// Unsupported receipts log filter prune mode for address. + #[error("receipts log filter for address {0} does not support distance prune mode")] + UnsupportedReceiptsLogFilterDistance(Address), } #[cfg(test)] diff --git a/crates/prune/types/src/target.rs b/crates/prune/types/src/target.rs index bb61c006cdc..1328079c850 100644 --- a/crates/prune/types/src/target.rs +++ b/crates/prune/types/src/target.rs @@ -2,7 +2,7 @@ use alloy_primitives::BlockNumber; use derive_more::Display; use thiserror::Error; -use crate::{PruneCheckpoint, PruneMode, PruneSegment}; +use crate::{PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig}; /// Minimum distance from the tip necessary for the node to work correctly: /// 1. Minimum 2 epochs (32 blocks per epoch) required to handle any reorg according to the @@ -99,10 +99,16 @@ pub struct PruneModes { ) )] pub merkle_changesets: PruneMode, - /// Receipts log filtering has been deprecated and will be removed in a future release. - #[deprecated] - #[cfg_attr(any(test, feature = "serde"), serde(skip))] - pub receipts_log_filter: (), + /// Receipts pruning configuration by retaining only those receipts that contain logs emitted + /// by the specified addresses, discarding others. This setting is overridden by `receipts`. + /// + /// The [`BlockNumber`](`crate::BlockNumber`) represents the starting block from which point + /// onwards the receipts are preserved. + #[cfg_attr( + any(test, feature = "serde"), + serde(skip_serializing_if = "ReceiptsLogPruneConfig::is_empty",) + )] + pub receipts_log_filter: ReceiptsLogPruneConfig, } impl Default for PruneModes { @@ -115,8 +121,7 @@ impl Default for PruneModes { storage_history: None, bodies_history: None, merkle_changesets: default_merkle_changesets_mode(), - #[expect(deprecated)] - receipts_log_filter: (), + receipts_log_filter: ReceiptsLogPruneConfig::new(), } } } @@ -132,14 +137,13 @@ impl PruneModes { storage_history: Some(PruneMode::Full), bodies_history: Some(PruneMode::Full), merkle_changesets: PruneMode::Full, - #[expect(deprecated)] - receipts_log_filter: (), + receipts_log_filter: ReceiptsLogPruneConfig::new(), } } /// Returns whether there is any kind of receipt pruning configuration. - pub const fn has_receipts_pruning(&self) -> bool { - self.receipts.is_some() + pub fn has_receipts_pruning(&self) -> bool { + self.receipts.is_some() || !self.receipts_log_filter.is_empty() } /// Returns an error if we can't unwind to the targeted block because the target block is @@ -265,7 +269,10 @@ fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserial #[cfg(test)] mod tests { + use std::collections::BTreeMap; + use super::*; + use alloy_primitives::address; use assert_matches::assert_matches; use serde::Deserialize; @@ -289,6 +296,30 @@ mod tests { ); } + #[test] + fn test_deserialize_receipts_log_filter() { + let addr1 = address!("0x0000000000000000000000000000000000000001"); + let addr2 = address!("0x0000000000000000000000000000000000000002"); + let addr3 = address!("0x0000000000000000000000000000000000000003"); + + let prune_modes: PruneModes = serde_json::from_str(&format!( + r#" + {{ + "receipts_log_filter": {{ + "{addr1}": "full", + "{addr2}": {{ "distance": 1000 }}, + "{addr3}": {{ "before": 5000000 }} + }} + }}"# + )) + .unwrap(); + + assert_eq!( + prune_modes.receipts_log_filter, + BTreeMap::from([(addr1, PruneMode::Full), (addr3, PruneMode::Before(5000000))]).into() + ); + } + #[test] fn test_unwind_target_unpruned() { // Test case 1: No pruning configured - should always succeed diff --git a/crates/stages/stages/src/stages/execution.rs b/crates/stages/stages/src/stages/execution.rs index adfc87c5ccc..573c59c0990 100644 --- a/crates/stages/stages/src/stages/execution.rs +++ b/crates/stages/stages/src/stages/execution.rs @@ -11,9 +11,9 @@ use reth_exex::{ExExManagerHandle, ExExNotification, ExExNotificationSource}; use reth_primitives_traits::{format_gas_throughput, BlockBody, NodePrimitives}; use reth_provider::{ providers::{StaticFileProvider, StaticFileWriter}, - BlockHashReader, BlockReader, DBProvider, ExecutionOutcome, HeaderProvider, + BlockHashReader, BlockReader, DBProvider, EitherWriter, ExecutionOutcome, HeaderProvider, LatestStateProviderRef, OriginalValuesKnown, ProviderError, StateWriter, - StaticFileProviderFactory, StatsReader, TransactionVariant, + StaticFileProviderFactory, StatsReader, StorageSettingsCache, TransactionVariant, }; use reth_revm::database::StateProviderDatabase; use reth_stages_api::{ @@ -185,11 +185,15 @@ where unwind_to: Option, ) -> Result<(), StageError> where - Provider: StaticFileProviderFactory + DBProvider + BlockReader + HeaderProvider, + Provider: StaticFileProviderFactory + + DBProvider + + BlockReader + + HeaderProvider + + StorageSettingsCache, { // If there's any receipts pruning configured, receipts are written directly to database and // inconsistencies are expected. - if provider.prune_modes_ref().has_receipts_pruning() { + if EitherWriter::receipts_destination(provider).is_database() { return Ok(()) } @@ -259,7 +263,8 @@ where Primitives: NodePrimitives, > + StatsReader + BlockHashReader - + StateWriter::Receipt>, + + StateWriter::Receipt> + + StorageSettingsCache, { /// Return the id of the stage fn id(&self) -> StageId { diff --git a/crates/static-file/static-file/src/static_file_producer.rs b/crates/static-file/static-file/src/static_file_producer.rs index 2e7aa4b9df4..03337f1fd7d 100644 --- a/crates/static-file/static-file/src/static_file_producer.rs +++ b/crates/static-file/static-file/src/static_file_producer.rs @@ -194,7 +194,9 @@ where let targets = StaticFileTargets { // StaticFile receipts only if they're not pruned according to the user configuration - receipts: if self.prune_modes.receipts.is_none() { + receipts: if self.prune_modes.receipts.is_none() && + self.prune_modes.receipts_log_filter.is_empty() + { finalized_block_numbers.receipts.and_then(|finalized_block_number| { self.get_static_file_target( highest_static_files.receipts, diff --git a/crates/storage/provider/src/either_writer.rs b/crates/storage/provider/src/either_writer.rs index 5c50141f651..36d88c03fe9 100644 --- a/crates/storage/provider/src/either_writer.rs +++ b/crates/storage/provider/src/either_writer.rs @@ -5,7 +5,9 @@ use alloy_primitives::{BlockNumber, TxNumber}; use reth_db::table::Value; use reth_db_api::{cursor::DbCursorRW, tables}; use reth_node_types::NodePrimitives; +use reth_storage_api::{DBProvider, StorageSettingsCache}; use reth_storage_errors::provider::ProviderResult; +use strum::EnumIs; /// Represents a destination for writing data, either to database or static files. #[derive(Debug)] @@ -16,6 +18,39 @@ pub enum EitherWriter<'a, CURSOR, N> { StaticFile(StaticFileProviderRWRefMut<'a, N>), } +impl EitherWriter<'_, (), ()> { + /// Returns the destination for writing receipts. + /// + /// The rules are as follows: + /// - If the node should not always write receipts to static files, and any receipt pruning is + /// enabled, write to the database. + /// - If the node should always write receipts to static files, but receipt log filter pruning + /// is enabled, write to the database. + /// - Otherwise, write to static files. + pub fn receipts_destination( + provider: &P, + ) -> EitherWriterDestination { + let receipts_in_static_files = provider.cached_storage_settings().receipts_in_static_files; + let prune_modes = provider.prune_modes_ref(); + + if !receipts_in_static_files && prune_modes.has_receipts_pruning() || + // TODO: support writing receipts to static files with log filter pruning enabled + receipts_in_static_files && !prune_modes.receipts_log_filter.is_empty() + { + EitherWriterDestination::Database + } else { + EitherWriterDestination::StaticFile + } + } +} + +#[derive(Debug, EnumIs)] +#[allow(missing_docs)] +pub enum EitherWriterDestination { + Database, + StaticFile, +} + impl<'a, CURSOR, N: NodePrimitives> EitherWriter<'a, CURSOR, N> { /// Increment the block number. /// diff --git a/crates/storage/provider/src/providers/database/mod.rs b/crates/storage/provider/src/providers/database/mod.rs index 02f11c6a1b8..7124bc01f72 100644 --- a/crates/storage/provider/src/providers/database/mod.rs +++ b/crates/storage/provider/src/providers/database/mod.rs @@ -113,7 +113,7 @@ impl ProviderFactory { impl ProviderFactory { /// Sets the pruning configuration for an existing [`ProviderFactory`]. - pub const fn with_prune_modes(mut self, prune_modes: PruneModes) -> Self { + pub fn with_prune_modes(mut self, prune_modes: PruneModes) -> Self { self.prune_modes = prune_modes; self } diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index ffde83de7e7..83a3715ecc5 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -22,7 +22,7 @@ use crate::{ }; use alloy_consensus::{ transaction::{SignerRecoverable, TransactionMeta, TxHashRef}, - BlockHeader, + BlockHeader, TxReceipt, }; use alloy_eips::BlockHashOrNumber; use alloy_primitives::{ @@ -52,9 +52,7 @@ use reth_node_types::{BlockTy, BodyTy, HeaderTy, NodeTypes, ReceiptTy, TxTy}; use reth_primitives_traits::{ Account, Block as _, BlockBody as _, Bytecode, RecoveredBlock, SealedHeader, StorageEntry, }; -use reth_prune_types::{ - PruneCheckpoint, PruneMode, PruneModes, PruneSegment, MINIMUM_PRUNING_DISTANCE, -}; +use reth_prune_types::{PruneCheckpoint, PruneModes, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; use reth_storage_api::{ @@ -218,7 +216,7 @@ impl DatabaseProvider { #[cfg(feature = "test-utils")] /// Sets the prune modes for provider. - pub const fn set_prune_modes(&mut self, prune_modes: PruneModes) { + pub fn set_prune_modes(&mut self, prune_modes: PruneModes) { self.prune_modes = prune_modes; } } @@ -369,7 +367,7 @@ impl DatabaseProvider>>(from_tx..)?; - if !self.prune_modes.has_receipts_pruning() { + if EitherWriter::receipts_destination(self).is_static_file() { let static_file_receipt_num = self.static_file_provider.get_highest_static_file_tx(StaticFileSegment::Receipts); @@ -1608,9 +1606,7 @@ impl StateWriter // Write receipts to static files only if they're explicitly enabled or we don't have // receipts pruning - let mut receipts_writer = if self.storage_settings.read().receipts_in_static_files || - !self.prune_modes.has_receipts_pruning() - { + let mut receipts_writer = if EitherWriter::receipts_destination(self).is_static_file() { EitherWriter::StaticFile( self.static_file_provider.get_writer(first_block, StaticFileSegment::Receipts)?, ) @@ -1618,10 +1614,14 @@ impl StateWriter EitherWriter::Database(self.tx.cursor_write::>()?) }; - // All receipts from the last 128 blocks are required for blockchain tree, even with - // [`PruneSegment::ContractLogs`]. - let prunable_receipts = - PruneMode::Distance(MINIMUM_PRUNING_DISTANCE).should_prune(first_block, tip); + let has_contract_log_filter = !self.prune_modes.receipts_log_filter.is_empty(); + let contract_log_pruner = self.prune_modes.receipts_log_filter.group_by_block(tip, None)?; + + // Prepare set of addresses which logs should not be pruned. + let mut allowed_addresses: HashSet = HashSet::new(); + for (_, addresses) in contract_log_pruner.range(..first_block) { + allowed_addresses.extend(addresses.iter().copied()); + } for (idx, (receipts, first_tx_index)) in execution_outcome.receipts.iter().zip(block_indices).enumerate() @@ -1632,17 +1632,26 @@ impl StateWriter receipts_writer.increment_block(block_number)?; // Skip writing receipts if pruning configuration requires us to. - if prunable_receipts && - self.prune_modes - .receipts - .is_some_and(|mode| mode.should_prune(block_number, tip)) - { + if self.prune_modes.receipts.is_some_and(|mode| mode.should_prune(block_number, tip)) { continue } + // If there are new addresses to retain after this block number, track them + if let Some(new_addresses) = contract_log_pruner.get(&block_number) { + allowed_addresses.extend(new_addresses.iter().copied()); + } + for (idx, receipt) in receipts.iter().enumerate() { let receipt_idx = first_tx_index + idx as u64; + // Skip writing receipt if log filter is active and it does not have any logs to + // retain + if has_contract_log_filter && + !receipt.logs().iter().any(|log| allowed_addresses.contains(&log.address)) + { + continue + } + receipts_writer.append_receipt(receipt_idx, receipt)?; } } diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index e29371a7025..9f2155f8806 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -808,6 +808,9 @@ Pruning: --prune.receipts.before Prune receipts before the specified block number. The specified block number is not pruned + --prune.receiptslogfilter + Receipts Log Filter + --prune.account-history.full Prunes all account history