From 0141cfd761a81faac87d876d0e14d807ca34237c Mon Sep 17 00:00:00 2001 From: joshieDo <93316087+joshieDo@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:13:25 +0000 Subject: [PATCH 1/3] fix(prune): add minimum 64 block retention for receipts and bodies When PruneMode::Full is used for receipts, the pruner would prune all receipts up to the tip block immediately after persistence. This causes a race condition during chain reorganizations: 1. Block A at height N is persisted to disk 2. Pruner runs and prunes A's receipts 3. Block B at height N arrives (reorg) 4. FCU tries to make B canonical 5. on_new_head calls canonical_block_by_hash(A) to walk back old chain 6. canonical_block_by_hash tries to reconstruct ExecutedBlock from disk 7. get_state() fails with 'no receipt found' because receipts were pruned This fix adds MINIMUM_RECEIPTS_DISTANCE (64 blocks) to ensure receipts and bodies are retained long enough to handle any potential reorgs. Fixes a regression introduced by PR #17938 which added canonical_block_by_hash that assumes ExecutedBlock can always be reconstructed from disk. --- crates/prune/types/src/lib.rs | 4 +++- crates/prune/types/src/segment.rs | 5 +++-- crates/prune/types/src/target.rs | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/prune/types/src/lib.rs b/crates/prune/types/src/lib.rs index 315063278b2..85499a4742c 100644 --- a/crates/prune/types/src/lib.rs +++ b/crates/prune/types/src/lib.rs @@ -30,7 +30,9 @@ pub use pruner::{ SegmentOutputCheckpoint, }; pub use segment::{PrunePurpose, PruneSegment, PruneSegmentError}; -pub use target::{PruneModes, UnwindTargetPrunedError, MINIMUM_PRUNING_DISTANCE}; +pub use target::{ + PruneModes, UnwindTargetPrunedError, MINIMUM_PRUNING_DISTANCE, MINIMUM_RECEIPTS_DISTANCE, +}; /// Configuration for pruning receipts not associated with logs emitted by the specified contracts. #[derive(Debug, Clone, PartialEq, Eq, Default)] diff --git a/crates/prune/types/src/segment.rs b/crates/prune/types/src/segment.rs index 0e3f4e1edc6..e142c6f4187 100644 --- a/crates/prune/types/src/segment.rs +++ b/crates/prune/types/src/segment.rs @@ -1,6 +1,6 @@ #![allow(deprecated)] // necessary to all defining deprecated `PruneSegment` variants -use crate::MINIMUM_PRUNING_DISTANCE; +use crate::{MINIMUM_PRUNING_DISTANCE, MINIMUM_RECEIPTS_DISTANCE}; use derive_more::Display; use strum::{EnumIter, IntoEnumIterator}; use thiserror::Error; @@ -65,7 +65,8 @@ impl PruneSegment { /// Returns minimum number of blocks to keep in the database for this segment. pub const fn min_blocks(&self) -> u64 { match self { - Self::SenderRecovery | Self::TransactionLookup | Self::Receipts | Self::Bodies => 0, + Self::SenderRecovery | Self::TransactionLookup => 0, + Self::Receipts | Self::Bodies => MINIMUM_RECEIPTS_DISTANCE, Self::ContractLogs | Self::AccountHistory | Self::StorageHistory => { MINIMUM_PRUNING_DISTANCE } diff --git a/crates/prune/types/src/target.rs b/crates/prune/types/src/target.rs index 92a01fc2e5b..1a118005fe6 100644 --- a/crates/prune/types/src/target.rs +++ b/crates/prune/types/src/target.rs @@ -11,6 +11,11 @@ use crate::{PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig}; /// unwind is required. pub const MINIMUM_PRUNING_DISTANCE: u64 = 32 * 2 + 10_000; +/// Minimum blocks to retain for receipts and bodies to ensure reorg safety. +/// This prevents pruning data that may be needed when handling chain reorganizations, +/// specifically when `canonical_block_by_hash` needs to reconstruct `ExecutedBlock` from disk. +pub const MINIMUM_RECEIPTS_DISTANCE: u64 = 64; + /// Type of history that can be pruned #[derive(Debug, Error, PartialEq, Eq, Clone)] pub enum UnwindTargetPrunedError { From f938e79514fe45e1ec0783353ad75376ba55952e Mon Sep 17 00:00:00 2001 From: joshieDo <93316087+joshieDo@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:29:40 +0000 Subject: [PATCH 2/3] refactor: rename MINIMUM_PRUNING_DISTANCE to MINIMUM_STATE_HISTORY_DISTANCE - MINIMUM_STATE_HISTORY_DISTANCE (10064): used for AccountHistory, StorageHistory, ContractLogs - MINIMUM_PRUNING_DISTANCE (64): used for Receipts, Bodies to ensure reorg safety --- crates/node/core/src/args/pruning.rs | 19 ++++++++++-------- .../src/segments/user/receipts_by_logs.rs | 6 +++--- crates/prune/types/src/lib.rs | 2 +- crates/prune/types/src/mode.rs | 20 +++++++++---------- crates/prune/types/src/segment.rs | 6 +++--- crates/prune/types/src/target.rs | 8 ++++---- .../src/providers/database/provider.rs | 6 +++--- docs/vocs/docs/pages/cli/op-reth/node.mdx | 2 +- docs/vocs/docs/pages/cli/reth/node.mdx | 2 +- 9 files changed, 37 insertions(+), 34 deletions(-) diff --git a/crates/node/core/src/args/pruning.rs b/crates/node/core/src/args/pruning.rs index 24575a8ff75..9eff92a2abf 100644 --- a/crates/node/core/src/args/pruning.rs +++ b/crates/node/core/src/args/pruning.rs @@ -5,7 +5,9 @@ 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, ReceiptsLogPruneConfig, MINIMUM_PRUNING_DISTANCE}; +use reth_prune_types::{ + PruneMode, PruneModes, ReceiptsLogPruneConfig, MINIMUM_UNWIND_SAFE_DISTANCE, +}; use std::{collections::BTreeMap, ops::Not, sync::OnceLock}; /// Global static pruning defaults @@ -68,9 +70,9 @@ impl Default for DefaultPruningValues { full_prune_modes: PruneModes { sender_recovery: Some(PruneMode::Full), transaction_lookup: None, - receipts: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), - account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), - storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), + receipts: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)), + account_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)), + storage_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)), // This field is ignored when full_bodies_history_use_pre_merge is true bodies_history: None, receipts_log_filter: Default::default(), @@ -80,9 +82,9 @@ impl Default for DefaultPruningValues { sender_recovery: Some(PruneMode::Full), transaction_lookup: Some(PruneMode::Full), receipts: Some(PruneMode::Full), - account_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), - storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), - bodies_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), + account_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)), + storage_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)), + bodies_history: Some(PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE)), receipts_log_filter: Default::default(), }, } @@ -93,7 +95,8 @@ impl Default for DefaultPruningValues { #[derive(Debug, Clone, Args, PartialEq, Eq, Default)] #[command(next_help_heading = "Pruning")] pub struct PruningArgs { - /// Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored. + /// Run full node. Only the most recent [`MINIMUM_UNWIND_SAFE_DISTANCE`] block states are + /// stored. #[arg(long, default_value_t = false, conflicts_with = "minimal")] pub full: bool, diff --git a/crates/prune/prune/src/segments/user/receipts_by_logs.rs b/crates/prune/prune/src/segments/user/receipts_by_logs.rs index 9e57bd2411a..591e77997e5 100644 --- a/crates/prune/prune/src/segments/user/receipts_by_logs.rs +++ b/crates/prune/prune/src/segments/user/receipts_by_logs.rs @@ -11,7 +11,7 @@ use reth_provider::{ }; use reth_prune_types::{ PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment, ReceiptsLogPruneConfig, SegmentOutput, - MINIMUM_PRUNING_DISTANCE, + MINIMUM_UNWIND_SAFE_DISTANCE, }; use tracing::{instrument, trace}; #[derive(Debug)] @@ -49,8 +49,8 @@ where fn prune(&self, provider: &Provider, input: PruneInput) -> Result { // Contract log filtering removes every receipt possible except the ones in the list. So, // for the other receipts it's as if they had a `PruneMode::Distance()` of - // `MINIMUM_PRUNING_DISTANCE`. - let to_block = PruneMode::Distance(MINIMUM_PRUNING_DISTANCE) + // `MINIMUM_UNWIND_SAFE_DISTANCE`. + let to_block = PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE) .prune_target_block(input.to_block, PruneSegment::ContractLogs, PrunePurpose::User)? .map(|(bn, _)| bn) .unwrap_or_default(); diff --git a/crates/prune/types/src/lib.rs b/crates/prune/types/src/lib.rs index 85499a4742c..88d2d6490e6 100644 --- a/crates/prune/types/src/lib.rs +++ b/crates/prune/types/src/lib.rs @@ -31,7 +31,7 @@ pub use pruner::{ }; pub use segment::{PrunePurpose, PruneSegment, PruneSegmentError}; pub use target::{ - PruneModes, UnwindTargetPrunedError, MINIMUM_PRUNING_DISTANCE, MINIMUM_RECEIPTS_DISTANCE, + PruneModes, UnwindTargetPrunedError, MINIMUM_DISTANCE, MINIMUM_UNWIND_SAFE_DISTANCE, }; /// Configuration for pruning receipts not associated with logs emitted by the specified contracts. diff --git a/crates/prune/types/src/mode.rs b/crates/prune/types/src/mode.rs index 8f5eaac0d7a..5e8a0ba6f07 100644 --- a/crates/prune/types/src/mode.rs +++ b/crates/prune/types/src/mode.rs @@ -85,7 +85,7 @@ impl PruneMode { #[cfg(test)] mod tests { use crate::{ - PruneMode, PrunePurpose, PruneSegment, PruneSegmentError, MINIMUM_PRUNING_DISTANCE, + PruneMode, PrunePurpose, PruneSegment, PruneSegmentError, MINIMUM_UNWIND_SAFE_DISTANCE, }; use assert_matches::assert_matches; use serde::Deserialize; @@ -96,7 +96,7 @@ mod tests { let segment = PruneSegment::AccountHistory; let tests = vec![ - // MINIMUM_PRUNING_DISTANCE makes this impossible + // MINIMUM_UNWIND_SAFE_DISTANCE makes this impossible (PruneMode::Full, Err(PruneSegmentError::Configuration(segment))), // Nothing to prune (PruneMode::Distance(tip + 1), Ok(None)), @@ -107,12 +107,12 @@ mod tests { // Nothing to prune (PruneMode::Before(tip + 1), Ok(None)), ( - PruneMode::Before(tip - MINIMUM_PRUNING_DISTANCE), - Ok(Some(tip - MINIMUM_PRUNING_DISTANCE - 1)), + PruneMode::Before(tip - MINIMUM_UNWIND_SAFE_DISTANCE), + Ok(Some(tip - MINIMUM_UNWIND_SAFE_DISTANCE - 1)), ), ( - PruneMode::Before(tip - MINIMUM_PRUNING_DISTANCE - 1), - Ok(Some(tip - MINIMUM_PRUNING_DISTANCE - 2)), + PruneMode::Before(tip - MINIMUM_UNWIND_SAFE_DISTANCE - 1), + Ok(Some(tip - MINIMUM_UNWIND_SAFE_DISTANCE - 2)), ), // Nothing to prune (PruneMode::Before(tip - 1), Ok(None)), @@ -146,13 +146,13 @@ mod tests { let tests = vec![ (PruneMode::Distance(tip + 1), 1, !should_prune), ( - PruneMode::Distance(MINIMUM_PRUNING_DISTANCE + 1), - tip - MINIMUM_PRUNING_DISTANCE - 1, + PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE + 1), + tip - MINIMUM_UNWIND_SAFE_DISTANCE - 1, !should_prune, ), ( - PruneMode::Distance(MINIMUM_PRUNING_DISTANCE + 1), - tip - MINIMUM_PRUNING_DISTANCE - 2, + PruneMode::Distance(MINIMUM_UNWIND_SAFE_DISTANCE + 1), + tip - MINIMUM_UNWIND_SAFE_DISTANCE - 2, should_prune, ), (PruneMode::Before(tip + 1), 1, should_prune), diff --git a/crates/prune/types/src/segment.rs b/crates/prune/types/src/segment.rs index e142c6f4187..5bc055f8296 100644 --- a/crates/prune/types/src/segment.rs +++ b/crates/prune/types/src/segment.rs @@ -1,6 +1,6 @@ #![allow(deprecated)] // necessary to all defining deprecated `PruneSegment` variants -use crate::{MINIMUM_PRUNING_DISTANCE, MINIMUM_RECEIPTS_DISTANCE}; +use crate::{MINIMUM_DISTANCE, MINIMUM_UNWIND_SAFE_DISTANCE}; use derive_more::Display; use strum::{EnumIter, IntoEnumIterator}; use thiserror::Error; @@ -66,9 +66,9 @@ impl PruneSegment { pub const fn min_blocks(&self) -> u64 { match self { Self::SenderRecovery | Self::TransactionLookup => 0, - Self::Receipts | Self::Bodies => MINIMUM_RECEIPTS_DISTANCE, + Self::Receipts | Self::Bodies => MINIMUM_DISTANCE, Self::ContractLogs | Self::AccountHistory | Self::StorageHistory => { - MINIMUM_PRUNING_DISTANCE + MINIMUM_UNWIND_SAFE_DISTANCE } #[expect(deprecated)] #[expect(clippy::match_same_arms)] diff --git a/crates/prune/types/src/target.rs b/crates/prune/types/src/target.rs index 1a118005fe6..7f5c383a652 100644 --- a/crates/prune/types/src/target.rs +++ b/crates/prune/types/src/target.rs @@ -9,12 +9,12 @@ use crate::{PruneCheckpoint, PruneMode, PruneSegment, ReceiptsLogPruneConfig}; /// consensus protocol. /// 2. Another 10k blocks to have a room for maneuver in case when things go wrong and a manual /// unwind is required. -pub const MINIMUM_PRUNING_DISTANCE: u64 = 32 * 2 + 10_000; +pub const MINIMUM_UNWIND_SAFE_DISTANCE: u64 = 32 * 2 + 10_000; /// Minimum blocks to retain for receipts and bodies to ensure reorg safety. /// This prevents pruning data that may be needed when handling chain reorganizations, /// specifically when `canonical_block_by_hash` needs to reconstruct `ExecutedBlock` from disk. -pub const MINIMUM_RECEIPTS_DISTANCE: u64 = 64; +pub const MINIMUM_DISTANCE: u64 = 64; /// Type of history that can be pruned #[derive(Debug, Error, PartialEq, Eq, Clone)] @@ -61,7 +61,7 @@ pub struct PruneModes { any(test, feature = "serde"), serde( skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::" + deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::" ) )] pub account_history: Option, @@ -70,7 +70,7 @@ pub struct PruneModes { any(test, feature = "serde"), serde( skip_serializing_if = "Option::is_none", - deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::" + deserialize_with = "deserialize_opt_prune_mode_with_min_blocks::" ) )] pub storage_history: Option, diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index 795dbc308b5..5a766892dcb 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -53,7 +53,7 @@ use reth_primitives_traits::{ Account, Block as _, BlockBody as _, Bytecode, RecoveredBlock, SealedHeader, StorageEntry, }; use reth_prune_types::{ - PruneCheckpoint, PruneMode, PruneModes, PruneSegment, MINIMUM_PRUNING_DISTANCE, + PruneCheckpoint, PruneMode, PruneModes, PruneSegment, MINIMUM_UNWIND_SAFE_DISTANCE, }; use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; @@ -367,7 +367,7 @@ impl DatabaseProvider { changeset_cache, pending_rocksdb_batches: Default::default(), commit_order, - minimum_pruning_distance: MINIMUM_PRUNING_DISTANCE, + minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE, metrics: metrics::DatabaseProviderMetrics::default(), } } @@ -957,7 +957,7 @@ impl DatabaseProvider { changeset_cache, pending_rocksdb_batches: Default::default(), commit_order: CommitOrder::Normal, - minimum_pruning_distance: MINIMUM_PRUNING_DISTANCE, + minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE, metrics: metrics::DatabaseProviderMetrics::default(), } } diff --git a/docs/vocs/docs/pages/cli/op-reth/node.mdx b/docs/vocs/docs/pages/cli/op-reth/node.mdx index 3fe597815c4..4bb23fb8b76 100644 --- a/docs/vocs/docs/pages/cli/op-reth/node.mdx +++ b/docs/vocs/docs/pages/cli/op-reth/node.mdx @@ -832,7 +832,7 @@ Dev testnet: Pruning: --full - Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored + Run full node. Only the most recent [`MINIMUM_UNWIND_SAFE_DISTANCE`] block states are stored --minimal Run minimal storage mode with maximum pruning and smaller static files. diff --git a/docs/vocs/docs/pages/cli/reth/node.mdx b/docs/vocs/docs/pages/cli/reth/node.mdx index 31896c640ac..1dfc9ea993b 100644 --- a/docs/vocs/docs/pages/cli/reth/node.mdx +++ b/docs/vocs/docs/pages/cli/reth/node.mdx @@ -832,7 +832,7 @@ Dev testnet: Pruning: --full - Run full node. Only the most recent [`MINIMUM_PRUNING_DISTANCE`] block states are stored + Run full node. Only the most recent [`MINIMUM_UNWIND_SAFE_DISTANCE`] block states are stored --minimal Run minimal storage mode with maximum pruning and smaller static files. From 5e89f6f7a012ab44ec1af65b7e607451c4847b19 Mon Sep 17 00:00:00 2001 From: joshieDo <93316087+joshieDo@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:44:25 +0000 Subject: [PATCH 3/3] fix: handle Full prune mode with min_blocks > 0 --- crates/prune/types/src/mode.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/prune/types/src/mode.rs b/crates/prune/types/src/mode.rs index 5e8a0ba6f07..3706094b5fe 100644 --- a/crates/prune/types/src/mode.rs +++ b/crates/prune/types/src/mode.rs @@ -41,8 +41,12 @@ impl PruneMode { segment: PruneSegment, purpose: PrunePurpose, ) -> Result, PruneSegmentError> { + let min_blocks = segment.min_blocks(); let result = match self { - Self::Full if segment.min_blocks() == 0 => Some((tip, *self)), + Self::Full if min_blocks == 0 => Some((tip, *self)), + // For segments with min_blocks > 0, Full mode behaves like Distance(min_blocks) + Self::Full if min_blocks <= tip => Some((tip - min_blocks, *self)), + Self::Full => None, // Nothing to prune yet Self::Distance(distance) if *distance > tip => None, // Nothing to prune yet Self::Distance(distance) if *distance >= segment.min_blocks() => { Some((tip - distance, *self)) @@ -84,9 +88,7 @@ impl PruneMode { #[cfg(test)] mod tests { - use crate::{ - PruneMode, PrunePurpose, PruneSegment, PruneSegmentError, MINIMUM_UNWIND_SAFE_DISTANCE, - }; + use crate::{PruneMode, PrunePurpose, PruneSegment, MINIMUM_UNWIND_SAFE_DISTANCE}; use assert_matches::assert_matches; use serde::Deserialize; @@ -96,8 +98,8 @@ mod tests { let segment = PruneSegment::AccountHistory; let tests = vec![ - // MINIMUM_UNWIND_SAFE_DISTANCE makes this impossible - (PruneMode::Full, Err(PruneSegmentError::Configuration(segment))), + // Full mode with min_blocks > 0 behaves like Distance(min_blocks) + (PruneMode::Full, Ok(Some(tip - segment.min_blocks()))), // Nothing to prune (PruneMode::Distance(tip + 1), Ok(None)), (