diff --git a/crates/chain-state/benches/canonical_hashes_range.rs b/crates/chain-state/benches/canonical_hashes_range.rs index 58fdd73bf99..c19ce25ec4f 100644 --- a/crates/chain-state/benches/canonical_hashes_range.rs +++ b/crates/chain-state/benches/canonical_hashes_range.rs @@ -2,7 +2,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use reth_chain_state::{ - test_utils::TestBlockBuilder, ExecutedBlockWithTrieUpdates, MemoryOverlayStateProviderRef, + test_utils::TestBlockBuilder, ExecutedBlock, MemoryOverlayStateProviderRef, }; use reth_ethereum_primitives::EthPrimitives; use reth_storage_api::{noop::NoopProvider, BlockHashReader}; @@ -84,10 +84,7 @@ fn bench_canonical_hashes_range(c: &mut Criterion) { fn setup_provider_with_blocks( num_blocks: usize, -) -> ( - MemoryOverlayStateProviderRef<'static, EthPrimitives>, - Vec>, -) { +) -> (MemoryOverlayStateProviderRef<'static, EthPrimitives>, Vec>) { let mut builder = TestBlockBuilder::::default(); let blocks: Vec<_> = builder.get_executed_blocks(1000..1000 + num_blocks as u64).collect(); diff --git a/crates/chain-state/src/in_memory.rs b/crates/chain-state/src/in_memory.rs index dd78b6cf5fe..5b2f666657b 100644 --- a/crates/chain-state/src/in_memory.rs +++ b/crates/chain-state/src/in_memory.rs @@ -242,7 +242,7 @@ impl CanonicalInMemoryState { /// Updates the pending block with the given block. /// /// Note: This assumes that the parent block of the pending block is canonical. - pub fn set_pending_block(&self, pending: ExecutedBlockWithTrieUpdates) { + pub fn set_pending_block(&self, pending: ExecutedBlock) { // fetch the state of the pending block's parent block let parent = self.state_by_hash(pending.recovered_block().parent_hash()); let pending = BlockState::with_parent(pending, parent); @@ -258,7 +258,7 @@ impl CanonicalInMemoryState { /// them to their parent blocks. fn update_blocks(&self, new_blocks: I, reorged: R) where - I: IntoIterator>, + I: IntoIterator>, R: IntoIterator>, { { @@ -568,22 +568,19 @@ impl CanonicalInMemoryState { #[derive(Debug, PartialEq, Eq, Clone)] pub struct BlockState { /// The executed block that determines the state after this block has been executed. - block: ExecutedBlockWithTrieUpdates, + block: ExecutedBlock, /// The block's parent block if it exists. parent: Option>>, } impl BlockState { /// [`BlockState`] constructor. - pub const fn new(block: ExecutedBlockWithTrieUpdates) -> Self { + pub const fn new(block: ExecutedBlock) -> Self { Self { block, parent: None } } /// [`BlockState`] constructor with parent. - pub const fn with_parent( - block: ExecutedBlockWithTrieUpdates, - parent: Option>, - ) -> Self { + pub const fn with_parent(block: ExecutedBlock, parent: Option>) -> Self { Self { block, parent } } @@ -597,12 +594,12 @@ impl BlockState { } /// Returns the executed block that determines the state. - pub fn block(&self) -> ExecutedBlockWithTrieUpdates { + pub fn block(&self) -> ExecutedBlock { self.block.clone() } /// Returns a reference to the executed block that determines the state. - pub const fn block_ref(&self) -> &ExecutedBlockWithTrieUpdates { + pub const fn block_ref(&self) -> &ExecutedBlock { &self.block } @@ -730,6 +727,8 @@ pub struct ExecutedBlock { pub execution_output: Arc>, /// Block's hashed state. pub hashed_state: Arc, + /// Trie updates that result from calculating the state root for the block. + pub trie_updates: Arc, } impl Default for ExecutedBlock { @@ -738,6 +737,7 @@ impl Default for ExecutedBlock { recovered_block: Default::default(), execution_output: Default::default(), hashed_state: Default::default(), + trie_updates: Default::default(), } } } @@ -767,113 +767,16 @@ impl ExecutedBlock { &self.hashed_state } - /// Returns a [`BlockNumber`] of the block. + /// Returns a reference to the trie updates resulting from the execution outcome #[inline] - pub fn block_number(&self) -> BlockNumber { - self.recovered_block.header().number() + pub fn trie_updates(&self) -> &TrieUpdates { + &self.trie_updates } -} -/// Trie updates that result from calculating the state root for the block. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ExecutedTrieUpdates { - /// Trie updates present. State root was calculated, and the trie updates can be applied to the - /// database. - Present(Arc), - /// Trie updates missing. State root was calculated, but the trie updates cannot be applied to - /// the current database state. To apply the updates, the state root must be recalculated, and - /// new trie updates must be generated. - /// - /// This can happen when processing fork chain blocks that are building on top of the - /// historical database state. Since we don't store the historical trie state, we cannot - /// generate the trie updates for it. - Missing, -} - -impl ExecutedTrieUpdates { - /// Creates a [`ExecutedTrieUpdates`] with present but empty trie updates. - pub fn empty() -> Self { - Self::Present(Arc::default()) - } - - /// Sets the trie updates to the provided value as present. - pub fn set_present(&mut self, updates: Arc) { - *self = Self::Present(updates); - } - - /// Takes the present trie updates, leaving the state as missing. - pub fn take_present(&mut self) -> Option> { - match self { - Self::Present(updates) => { - let updates = core::mem::take(updates); - *self = Self::Missing; - Some(updates) - } - Self::Missing => None, - } - } - - /// Returns a reference to the trie updates if present. - #[allow(clippy::missing_const_for_fn)] // false positive - pub fn as_ref(&self) -> Option<&TrieUpdates> { - match self { - Self::Present(updates) => Some(updates), - Self::Missing => None, - } - } - - /// Returns `true` if the trie updates are present. - pub const fn is_present(&self) -> bool { - matches!(self, Self::Present(_)) - } - - /// Returns `true` if the trie updates are missing. - pub const fn is_missing(&self) -> bool { - matches!(self, Self::Missing) - } -} - -/// An [`ExecutedBlock`] with its [`TrieUpdates`]. -/// -/// We store it as separate type because [`TrieUpdates`] are only available for blocks stored in -/// memory and can't be obtained for canonical persisted blocks. -#[derive( - Clone, Debug, PartialEq, Eq, derive_more::Deref, derive_more::DerefMut, derive_more::Into, -)] -pub struct ExecutedBlockWithTrieUpdates { - /// Inner [`ExecutedBlock`]. - #[deref] - #[deref_mut] - #[into] - pub block: ExecutedBlock, - /// Trie updates that result from calculating the state root for the block. - /// - /// If [`ExecutedTrieUpdates::Missing`], the trie updates should be computed when persisting - /// the block **on top of the canonical parent**. - pub trie: ExecutedTrieUpdates, -} - -impl ExecutedBlockWithTrieUpdates { - /// [`ExecutedBlock`] constructor. - pub const fn new( - recovered_block: Arc>, - execution_output: Arc>, - hashed_state: Arc, - trie: ExecutedTrieUpdates, - ) -> Self { - Self { block: ExecutedBlock { recovered_block, execution_output, hashed_state }, trie } - } - - /// Returns a reference to the trie updates for the block, if present. + /// Returns a [`BlockNumber`] of the block. #[inline] - pub fn trie_updates(&self) -> Option<&TrieUpdates> { - self.trie.as_ref() - } - - /// Converts the value into [`SealedBlock`]. - pub fn into_sealed_block(self) -> SealedBlock { - let block = Arc::unwrap_or_clone(self.block.recovered_block); - block.into_sealed_block() + pub fn block_number(&self) -> BlockNumber { + self.recovered_block.header().number() } } @@ -883,18 +786,14 @@ pub enum NewCanonicalChain { /// A simple append to the current canonical head Commit { /// all blocks that lead back to the canonical head - new: Vec>, + new: Vec>, }, /// A reorged chain consists of two chains that trace back to a shared ancestor block at which /// point they diverge. Reorg { /// All blocks of the _new_ chain - new: Vec>, + new: Vec>, /// All blocks of the _old_ chain - /// - /// These are not [`ExecutedBlockWithTrieUpdates`] because we don't always have the trie - /// updates for the old canonical chain. For example, in case of node being restarted right - /// before the reorg [`TrieUpdates`] can't be fetched from database. old: Vec>, }, } @@ -1257,7 +1156,7 @@ mod tests { block1.recovered_block().hash() ); - let chain = NewCanonicalChain::Reorg { new: vec![block2.clone()], old: vec![block1.block] }; + let chain = NewCanonicalChain::Reorg { new: vec![block2.clone()], old: vec![block1] }; state.update_chain(chain); assert_eq!( state.head_state().unwrap().block_ref().recovered_block().hash(), @@ -1539,7 +1438,7 @@ mod tests { // Test reorg notification let chain_reorg = NewCanonicalChain::Reorg { new: vec![block1a.clone(), block2a.clone()], - old: vec![block1.block.clone(), block2.block.clone()], + old: vec![block1.clone(), block2.clone()], }; assert_eq!( diff --git a/crates/chain-state/src/memory_overlay.rs b/crates/chain-state/src/memory_overlay.rs index a035d833a46..2e1efd1ed1b 100644 --- a/crates/chain-state/src/memory_overlay.rs +++ b/crates/chain-state/src/memory_overlay.rs @@ -1,4 +1,4 @@ -use super::ExecutedBlockWithTrieUpdates; +use super::ExecutedBlock; use alloy_consensus::BlockHeader; use alloy_primitives::{keccak256, Address, BlockNumber, Bytes, StorageKey, StorageValue, B256}; use reth_errors::ProviderResult; @@ -24,7 +24,7 @@ pub struct MemoryOverlayStateProviderRef< /// Historical state provider for state lookups that are not found in memory blocks. pub(crate) historical: Box, /// The collection of executed parent blocks. Expected order is newest to oldest. - pub(crate) in_memory: Vec>, + pub(crate) in_memory: Vec>, /// Lazy-loaded in-memory trie data. pub(crate) trie_input: OnceLock, } @@ -41,10 +41,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> { /// - `in_memory` - the collection of executed ancestor blocks in reverse. /// - `historical` - a historical state provider for the latest ancestor block stored in the /// database. - pub fn new( - historical: Box, - in_memory: Vec>, - ) -> Self { + pub fn new(historical: Box, in_memory: Vec>) -> Self { Self { historical, in_memory, trie_input: OnceLock::new() } } @@ -60,7 +57,7 @@ impl<'a, N: NodePrimitives> MemoryOverlayStateProviderRef<'a, N> { self.in_memory .iter() .rev() - .map(|block| (block.hashed_state.as_ref(), block.trie.as_ref())), + .map(|block| (block.hashed_state.as_ref(), block.trie_updates.as_ref())), ) }) } diff --git a/crates/chain-state/src/test_utils.rs b/crates/chain-state/src/test_utils.rs index ace30b9cb35..5d318aca56c 100644 --- a/crates/chain-state/src/test_utils.rs +++ b/crates/chain-state/src/test_utils.rs @@ -1,6 +1,6 @@ use crate::{ - in_memory::ExecutedBlockWithTrieUpdates, CanonStateNotification, CanonStateNotifications, - CanonStateSubscriptions, ExecutedTrieUpdates, + in_memory::ExecutedBlock, CanonStateNotification, CanonStateNotifications, + CanonStateSubscriptions, }; use alloy_consensus::{Header, SignableTransaction, TxEip1559, TxReceipt, EMPTY_ROOT_HASH}; use alloy_eips::{ @@ -23,7 +23,7 @@ use reth_primitives_traits::{ SignedTransaction, }; use reth_storage_api::NodePrimitivesProvider; -use reth_trie::{root::state_root_unhashed, HashedPostState}; +use reth_trie::{root::state_root_unhashed, updates::TrieUpdates, HashedPostState}; use revm_database::BundleState; use revm_state::AccountInfo; use std::{ @@ -198,45 +198,45 @@ impl TestBlockBuilder { fork } - /// Gets an [`ExecutedBlockWithTrieUpdates`] with [`BlockNumber`], receipts and parent hash. + /// Gets an [`ExecutedBlock`] with [`BlockNumber`], receipts and parent hash. fn get_executed_block( &mut self, block_number: BlockNumber, receipts: Vec>, parent_hash: B256, - ) -> ExecutedBlockWithTrieUpdates { + ) -> ExecutedBlock { let block_with_senders = self.generate_random_block(block_number, parent_hash); let (block, senders) = block_with_senders.split_sealed(); - ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed(block, senders)), - Arc::new(ExecutionOutcome::new( + ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed(block, senders)), + execution_output: Arc::new(ExecutionOutcome::new( BundleState::default(), receipts, block_number, vec![Requests::default()], )), - Arc::new(HashedPostState::default()), - ExecutedTrieUpdates::empty(), - ) + hashed_state: Arc::new(HashedPostState::default()), + trie_updates: Arc::new(TrieUpdates::default()), + } } - /// Generates an [`ExecutedBlockWithTrieUpdates`] that includes the given receipts. + /// Generates an [`ExecutedBlock`] that includes the given receipts. pub fn get_executed_block_with_receipts( &mut self, receipts: Vec>, parent_hash: B256, - ) -> ExecutedBlockWithTrieUpdates { + ) -> ExecutedBlock { let number = rand::rng().random::(); self.get_executed_block(number, receipts, parent_hash) } - /// Generates an [`ExecutedBlockWithTrieUpdates`] with the given [`BlockNumber`]. + /// Generates an [`ExecutedBlock`] with the given [`BlockNumber`]. pub fn get_executed_block_with_number( &mut self, block_number: BlockNumber, parent_hash: B256, - ) -> ExecutedBlockWithTrieUpdates { + ) -> ExecutedBlock { self.get_executed_block(block_number, vec![vec![]], parent_hash) } @@ -244,7 +244,7 @@ impl TestBlockBuilder { pub fn get_executed_blocks( &mut self, range: Range, - ) -> impl Iterator + '_ { + ) -> impl Iterator + '_ { let mut parent_hash = B256::default(); range.map(move |number| { let current_parent_hash = parent_hash; diff --git a/crates/cli/commands/src/stage/drop.rs b/crates/cli/commands/src/stage/drop.rs index 66227e10271..5a01ad1fed6 100644 --- a/crates/cli/commands/src/stage/drop.rs +++ b/crates/cli/commands/src/stage/drop.rs @@ -15,7 +15,7 @@ use reth_db_common::{ }; use reth_node_api::{HeaderTy, ReceiptTy, TxTy}; use reth_node_core::args::StageEnum; -use reth_provider::{DBProvider, DatabaseProviderFactory, StaticFileProviderFactory}; +use reth_provider::{DBProvider, DatabaseProviderFactory, StaticFileProviderFactory, TrieWriter}; use reth_prune::PruneSegment; use reth_stages::StageId; use reth_static_file_types::StaticFileSegment; @@ -138,6 +138,10 @@ impl Command { None, )?; } + StageEnum::MerkleChangeSets => { + provider_rw.clear_trie_changesets()?; + reset_stage_checkpoint(tx, StageId::MerkleChangeSets)?; + } StageEnum::AccountHistory | StageEnum::StorageHistory => { tx.clear::()?; tx.clear::()?; diff --git a/crates/cli/commands/src/stage/unwind.rs b/crates/cli/commands/src/stage/unwind.rs index 9ef2085a065..ba9a00b11e2 100644 --- a/crates/cli/commands/src/stage/unwind.rs +++ b/crates/cli/commands/src/stage/unwind.rs @@ -15,10 +15,7 @@ use reth_db::DatabaseEnv; use reth_downloaders::{bodies::noop::NoopBodiesDownloader, headers::noop::NoopHeaderDownloader}; use reth_evm::ConfigureEvm; use reth_exex::ExExManagerHandle; -use reth_provider::{ - providers::ProviderNodeTypes, BlockExecutionWriter, BlockNumReader, ChainStateBlockReader, - ChainStateBlockWriter, ProviderFactory, StaticFileProviderFactory, -}; +use reth_provider::{providers::ProviderNodeTypes, BlockNumReader, ProviderFactory}; use reth_stages::{ sets::{DefaultStages, OfflineStages}, stages::ExecutionStage, @@ -60,54 +57,21 @@ impl> Command let components = components(provider_factory.chain_spec()); - let highest_static_file_block = provider_factory - .static_file_provider() - .get_highest_static_files() - .max_block_num() - .filter(|highest_static_file_block| *highest_static_file_block > target); - - // Execute a pipeline unwind if the start of the range overlaps the existing static - // files. If that's the case, then copy all available data from MDBX to static files, and - // only then, proceed with the unwind. - // - // We also execute a pipeline unwind if `offline` is specified, because we need to only - // unwind the data associated with offline stages. - if highest_static_file_block.is_some() || self.offline { - if self.offline { - info!(target: "reth::cli", "Performing an unwind for offline-only data!"); - } - - if let Some(highest_static_file_block) = highest_static_file_block { - info!(target: "reth::cli", ?target, ?highest_static_file_block, "Executing a pipeline unwind."); - } else { - info!(target: "reth::cli", ?target, "Executing a pipeline unwind."); - } - info!(target: "reth::cli", prune_config=?config.prune, "Using prune settings"); - - // This will build an offline-only pipeline if the `offline` flag is enabled - let mut pipeline = - self.build_pipeline(config, provider_factory, components.evm_config().clone())?; - - // Move all applicable data from database to static files. - pipeline.move_to_static_files()?; + if self.offline { + info!(target: "reth::cli", "Performing an unwind for offline-only data!"); + } - pipeline.unwind(target, None)?; - } else { - info!(target: "reth::cli", ?target, "Executing a database unwind."); - let provider = provider_factory.provider_rw()?; + let highest_static_file_block = provider_factory.provider()?.last_block_number()?; + info!(target: "reth::cli", ?target, ?highest_static_file_block, prune_config=?config.prune, "Executing a pipeline unwind."); - provider - .remove_block_and_execution_above(target) - .map_err(|err| eyre::eyre!("Transaction error on unwind: {err}"))?; + // This will build an offline-only pipeline if the `offline` flag is enabled + let mut pipeline = + self.build_pipeline(config, provider_factory, components.evm_config().clone())?; - // update finalized block if needed - let last_saved_finalized_block_number = provider.last_finalized_block_number()?; - if last_saved_finalized_block_number.is_none_or(|f| f > target) { - provider.save_finalized_block_number(target)?; - } + // Move all applicable data from database to static files. + pipeline.move_to_static_files()?; - provider.commit()?; - } + pipeline.unwind(target, None)?; info!(target: "reth::cli", ?target, "Unwound blocks"); diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs index c1c5ef96075..7ea5569834c 100644 --- a/crates/config/src/config.rs +++ b/crates/config/src/config.rs @@ -440,7 +440,7 @@ pub struct PruneConfig { impl Default for PruneConfig { fn default() -> Self { - Self { block_interval: DEFAULT_BLOCK_INTERVAL, segments: PruneModes::none() } + Self { block_interval: DEFAULT_BLOCK_INTERVAL, segments: PruneModes::default() } } } @@ -464,6 +464,7 @@ impl PruneConfig { account_history, storage_history, bodies_history, + merkle_changesets, receipts_log_filter, }, } = other; @@ -480,6 +481,8 @@ impl PruneConfig { self.segments.account_history = self.segments.account_history.or(account_history); self.segments.storage_history = self.segments.storage_history.or(storage_history); 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; @@ -1001,6 +1004,7 @@ receipts = 'full' account_history: None, storage_history: Some(PruneMode::Before(5000)), bodies_history: None, + merkle_changesets: PruneMode::Before(0), receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([( Address::random(), PruneMode::Full, @@ -1017,6 +1021,7 @@ receipts = 'full' account_history: Some(PruneMode::Distance(2000)), storage_history: Some(PruneMode::Distance(3000)), bodies_history: None, + merkle_changesets: PruneMode::Distance(10000), receipts_log_filter: ReceiptsLogPruneConfig(BTreeMap::from([ (Address::random(), PruneMode::Distance(1000)), (Address::random(), PruneMode::Before(2000)), @@ -1035,6 +1040,7 @@ receipts = 'full' assert_eq!(config1.segments.receipts, Some(PruneMode::Distance(1000))); 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_filter); } diff --git a/crates/engine/primitives/src/event.rs b/crates/engine/primitives/src/event.rs index 1c74282cba5..8cced031524 100644 --- a/crates/engine/primitives/src/event.rs +++ b/crates/engine/primitives/src/event.rs @@ -10,7 +10,7 @@ use core::{ fmt::{Display, Formatter, Result}, time::Duration, }; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_ethereum_primitives::EthPrimitives; use reth_primitives_traits::{NodePrimitives, SealedBlock, SealedHeader}; @@ -24,11 +24,11 @@ pub enum ConsensusEngineEvent { /// The fork choice state was updated, and the current fork choice status ForkchoiceUpdated(ForkchoiceState, ForkchoiceStatus), /// A block was added to the fork chain. - ForkBlockAdded(ExecutedBlockWithTrieUpdates, Duration), + ForkBlockAdded(ExecutedBlock, Duration), /// A new block was received from the consensus engine BlockReceived(BlockNumHash), /// A block was added to the canonical chain, and the elapsed time validating the block - CanonicalBlockAdded(ExecutedBlockWithTrieUpdates, Duration), + CanonicalBlockAdded(ExecutedBlock, Duration), /// A canonical chain was committed, and the elapsed time committing the data CanonicalChainCommitted(Box>, Duration), /// The consensus engine processed an invalid block. diff --git a/crates/engine/tree/src/engine.rs b/crates/engine/tree/src/engine.rs index bee52a46438..f08195b205e 100644 --- a/crates/engine/tree/src/engine.rs +++ b/crates/engine/tree/src/engine.rs @@ -7,7 +7,7 @@ use crate::{ }; use alloy_primitives::B256; use futures::{Stream, StreamExt}; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_engine_primitives::{BeaconEngineMessage, ConsensusEngineEvent}; use reth_ethereum_primitives::EthPrimitives; use reth_payload_primitives::PayloadTypes; @@ -246,7 +246,7 @@ pub enum EngineApiRequest { /// A request received from the consensus engine. Beacon(BeaconEngineMessage), /// Request to insert an already executed block, e.g. via payload building. - InsertExecutedBlock(ExecutedBlockWithTrieUpdates), + InsertExecutedBlock(ExecutedBlock), } impl Display for EngineApiRequest { diff --git a/crates/engine/tree/src/persistence.rs b/crates/engine/tree/src/persistence.rs index de5b10c331c..751356fc399 100644 --- a/crates/engine/tree/src/persistence.rs +++ b/crates/engine/tree/src/persistence.rs @@ -1,7 +1,7 @@ use crate::metrics::PersistenceMetrics; use alloy_consensus::BlockHeader; use alloy_eips::BlockNumHash; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_errors::ProviderError; use reth_ethereum_primitives::EthPrimitives; use reth_primitives_traits::NodePrimitives; @@ -140,7 +140,7 @@ where fn on_save_blocks( &self, - blocks: Vec>, + blocks: Vec>, ) -> Result, PersistenceError> { debug!(target: "engine::persistence", first=?blocks.first().map(|b| b.recovered_block.num_hash()), last=?blocks.last().map(|b| b.recovered_block.num_hash()), "Saving range of blocks"); let start_time = Instant::now(); @@ -180,7 +180,7 @@ pub enum PersistenceAction { /// /// First, header, transaction, and receipt-related data should be written to static files. /// Then the execution history-related data will be written to the database. - SaveBlocks(Vec>, oneshot::Sender>), + SaveBlocks(Vec>, oneshot::Sender>), /// Removes block data above the given block number from the database. /// @@ -257,7 +257,7 @@ impl PersistenceHandle { /// If there are no blocks to persist, then `None` is sent in the sender. pub fn save_blocks( &self, - blocks: Vec>, + blocks: Vec>, tx: oneshot::Sender>, ) -> Result<(), SendError>> { self.send_action(PersistenceAction::SaveBlocks(blocks, tx)) diff --git a/crates/engine/tree/src/tree/error.rs b/crates/engine/tree/src/tree/error.rs index f7b1111df06..8589bc59d3d 100644 --- a/crates/engine/tree/src/tree/error.rs +++ b/crates/engine/tree/src/tree/error.rs @@ -1,7 +1,6 @@ //! Internal errors for the tree module. use alloy_consensus::BlockHeader; -use alloy_primitives::B256; use reth_consensus::ConsensusError; use reth_errors::{BlockExecutionError, BlockValidationError, ProviderError}; use reth_evm::execute::InternalBlockExecutionError; @@ -19,20 +18,6 @@ pub enum AdvancePersistenceError { /// A provider error #[error(transparent)] Provider(#[from] ProviderError), - /// Missing ancestor. - /// - /// This error occurs when we need to compute the state root for a block with missing trie - /// updates, but the ancestor block is not available. State root computation requires the state - /// from the parent block as a starting point. - /// - /// A block may be missing the trie updates when it's a fork chain block building on top of the - /// historical database state. Since we don't store the historical trie state, we cannot - /// generate the trie updates for it until the moment when database is unwound to the canonical - /// chain. - /// - /// Also see [`reth_chain_state::ExecutedTrieUpdates::Missing`]. - #[error("Missing ancestor with hash {0}")] - MissingAncestor(B256), } #[derive(thiserror::Error)] diff --git a/crates/engine/tree/src/tree/mod.rs b/crates/engine/tree/src/tree/mod.rs index 7f1183f5efc..e66b2a8892e 100644 --- a/crates/engine/tree/src/tree/mod.rs +++ b/crates/engine/tree/src/tree/mod.rs @@ -13,10 +13,8 @@ use alloy_rpc_types_engine::{ ForkchoiceState, PayloadStatus, PayloadStatusEnum, PayloadValidationError, }; use error::{InsertBlockError, InsertBlockFatalError}; -use persistence_state::CurrentPersistenceAction; use reth_chain_state::{ - CanonicalInMemoryState, ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates, - MemoryOverlayStateProvider, NewCanonicalChain, + CanonicalInMemoryState, ExecutedBlock, MemoryOverlayStateProvider, NewCanonicalChain, }; use reth_consensus::{Consensus, FullConsensus}; use reth_engine_primitives::{ @@ -31,14 +29,12 @@ use reth_payload_primitives::{ }; use reth_primitives_traits::{NodePrimitives, RecoveredBlock, SealedBlock, SealedHeader}; use reth_provider::{ - providers::ConsistentDbView, BlockNumReader, BlockReader, DBProvider, DatabaseProviderFactory, - HashedPostStateProvider, ProviderError, StateProviderBox, StateProviderFactory, StateReader, - StateRootProvider, TransactionVariant, + providers::ConsistentDbView, BlockReader, DatabaseProviderFactory, HashedPostStateProvider, + ProviderError, StateProviderBox, StateProviderFactory, StateReader, TransactionVariant, + TrieReader, }; use reth_revm::database::StateProviderDatabase; use reth_stages_api::ControlFlow; -use reth_trie::{HashedPostState, TrieInput}; -use reth_trie_db::DatabaseHashedPostState; use revm::state::EvmState; use state::TreeState; use std::{ @@ -78,7 +74,6 @@ pub use payload_processor::*; pub use payload_validator::{BasicEngineValidator, EngineValidator}; pub use persistence_state::PersistenceState; pub use reth_engine_primitives::TreeConfig; -use reth_trie::KeccakKeyHasher; pub mod state; @@ -101,7 +96,7 @@ pub struct StateProviderBuilder { /// The historical block hash to fetch state from. historical: B256, /// The blocks that form the chain from historical to target and are in memory. - overlay: Option>>, + overlay: Option>>, } impl StateProviderBuilder { @@ -110,7 +105,7 @@ impl StateProviderBuilder { pub const fn new( provider_factory: P, historical: B256, - overlay: Option>>, + overlay: Option>>, ) -> Self { Self { provider_factory, historical, overlay } } @@ -318,6 +313,7 @@ where + StateProviderFactory + StateReader + HashedPostStateProvider + + TrieReader + Clone + 'static,

::Provider: @@ -823,7 +819,7 @@ where for block_num in (new_head_number + 1)..=current_head_number { if let Some(block_state) = self.canonical_in_memory_state.state_by_number(block_num) { - let executed_block = block_state.block_ref().block.clone(); + let executed_block = block_state.block_ref().clone(); old_blocks.push(executed_block); debug!( target: "engine::tree", @@ -855,14 +851,9 @@ where // Try to load the canonical ancestor's block match self.canonical_block_by_hash(new_head_hash)? { Some(executed_block) => { - let block_with_trie = ExecutedBlockWithTrieUpdates { - block: executed_block, - trie: ExecutedTrieUpdates::Missing, - }; - // Perform the reorg to properly handle the unwind self.canonical_in_memory_state.update_chain(NewCanonicalChain::Reorg { - new: vec![block_with_trie], + new: vec![executed_block], old: old_blocks, }); @@ -915,13 +906,8 @@ where // Try to load the block from storage if let Some(executed_block) = self.canonical_block_by_hash(block_hash)? { - let block_with_trie = ExecutedBlockWithTrieUpdates { - block: executed_block, - trie: ExecutedTrieUpdates::Missing, - }; - self.canonical_in_memory_state - .update_chain(NewCanonicalChain::Commit { new: vec![block_with_trie] }); + .update_chain(NewCanonicalChain::Commit { new: vec![executed_block] }); debug!( target: "engine::tree", @@ -976,29 +962,6 @@ where Ok(true) } - /// Returns the persisting kind for the input block. - fn persisting_kind_for(&self, block: BlockWithParent) -> PersistingKind { - // Check that we're currently persisting. - let Some(action) = self.persistence_state.current_action() else { - return PersistingKind::NotPersisting - }; - // Check that the persistince action is saving blocks, not removing them. - let CurrentPersistenceAction::SavingBlocks { highest } = action else { - return PersistingKind::PersistingNotDescendant - }; - - // The block being validated can only be a descendant if its number is higher than - // the highest block persisting. Otherwise, it's likely a fork of a lower block. - if block.block.number > highest.number && - self.state.tree_state.is_descendant(*highest, block) - { - return PersistingKind::PersistingDescendant - } - - // In all other cases, the block is not a descendant. - PersistingKind::PersistingNotDescendant - } - /// Invoked when we receive a new forkchoice update message. Calls into the blockchain tree /// to resolve chain forks and ensure that the Execution Layer is working with the latest valid /// chain. @@ -1305,7 +1268,7 @@ where /// Helper method to save blocks and set the persistence state. This ensures we keep track of /// the current persistence action while we're saving blocks. - fn persist_blocks(&mut self, blocks_to_persist: Vec>) { + fn persist_blocks(&mut self, blocks_to_persist: Vec>) { if blocks_to_persist.is_empty() { debug!(target: "engine::tree", "Returned empty set of blocks to persist"); return @@ -1696,17 +1659,9 @@ where /// Returns a batch of consecutive canonical blocks to persist in the range /// `(last_persisted_number .. canonical_head - threshold]`. The expected /// order is oldest -> newest. - /// - /// If any blocks are missing trie updates, all blocks are persisted, not taking `threshold` - /// into account. - /// - /// For those blocks that didn't have the trie updates calculated, runs the state root - /// calculation, and saves the trie updates. - /// - /// Returns an error if the state root calculation fails. fn get_canonical_blocks_to_persist( - &mut self, - ) -> Result>, AdvancePersistenceError> { + &self, + ) -> Result>, AdvancePersistenceError> { // We will calculate the state root using the database, so we need to be sure there are no // changes debug_assert!(!self.persistence_state.in_progress()); @@ -1715,27 +1670,16 @@ where let mut current_hash = self.state.tree_state.canonical_block_hash(); let last_persisted_number = self.persistence_state.last_persisted_block.number; let canonical_head_number = self.state.tree_state.canonical_block_number(); - let all_blocks_have_trie_updates = self - .state - .tree_state - .blocks_by_hash - .values() - .all(|block| block.trie_updates().is_some()); - - let target_number = if all_blocks_have_trie_updates { - // Persist only up to block buffer target if all blocks have trie updates - canonical_head_number.saturating_sub(self.config.memory_block_buffer_target()) - } else { - // Persist all blocks if any block is missing trie updates - canonical_head_number - }; + + // Persist only up to block buffer target + let target_number = + canonical_head_number.saturating_sub(self.config.memory_block_buffer_target()); debug!( target: "engine::tree", ?current_hash, ?last_persisted_number, ?canonical_head_number, - ?all_blocks_have_trie_updates, ?target_number, "Returning canonical blocks to persist" ); @@ -1754,48 +1698,6 @@ where // Reverse the order so that the oldest block comes first blocks_to_persist.reverse(); - // Calculate missing trie updates - for block in &mut blocks_to_persist { - if block.trie.is_present() { - continue - } - - debug!( - target: "engine::tree", - block = ?block.recovered_block().num_hash(), - "Calculating trie updates before persisting" - ); - - let provider = self - .state_provider_builder(block.recovered_block().parent_hash())? - .ok_or(AdvancePersistenceError::MissingAncestor( - block.recovered_block().parent_hash(), - ))? - .build()?; - - let mut trie_input = self.compute_trie_input( - self.persisting_kind_for(block.recovered_block.block_with_parent()), - self.provider.database_provider_ro()?, - block.recovered_block().parent_hash(), - None, - )?; - // Extend with block we are generating trie updates for. - trie_input.append_ref(block.hashed_state()); - let (_root, updates) = provider.state_root_from_nodes_with_updates(trie_input)?; - debug_assert_eq!(_root, block.recovered_block().state_root()); - - // Update trie updates in both tree state and blocks to persist that we return - let trie_updates = Arc::new(updates); - let tree_state_block = self - .state - .tree_state - .blocks_by_hash - .get_mut(&block.recovered_block().hash()) - .expect("blocks to persist are constructed from tree state blocks"); - tree_state_block.trie.set_present(trie_updates.clone()); - block.trie.set_present(trie_updates); - } - Ok(blocks_to_persist) } @@ -1834,7 +1736,7 @@ where trace!(target: "engine::tree", ?hash, "Fetching executed block by hash"); // check memory first if let Some(block) = self.state.tree_state.executed_block_by_hash(hash) { - return Ok(Some(block.block.clone())) + return Ok(Some(block.clone())) } let (block, senders) = self @@ -1847,11 +1749,13 @@ where .get_state(block.header().number())? .ok_or_else(|| ProviderError::StateForNumberNotFound(block.header().number()))?; let hashed_state = self.provider.hashed_post_state(execution_output.state()); + let trie_updates = self.provider.get_block_trie_updates(block.number())?; Ok(Some(ExecutedBlock { recovered_block: Arc::new(RecoveredBlock::new_sealed(block, senders)), execution_output: Arc::new(execution_output), hashed_state: Arc::new(hashed_state), + trie_updates: Arc::new(trie_updates.into()), })) } @@ -2289,25 +2193,7 @@ where self.update_reorg_metrics(old.len()); self.reinsert_reorged_blocks(new.clone()); - // Try reinserting the reorged canonical chain. This is only possible if we have - // `persisted_trie_updates` for those blocks. - let old = old - .iter() - .filter_map(|block| { - let trie = self - .state - .tree_state - .persisted_trie_updates - .get(&block.recovered_block.hash())? - .1 - .clone(); - Some(ExecutedBlockWithTrieUpdates { - block: block.clone(), - trie: ExecutedTrieUpdates::Present(trie), - }) - }) - .collect::>(); - self.reinsert_reorged_blocks(old); + self.reinsert_reorged_blocks(old.clone()); } // update the tracked in-memory state with the new chain @@ -2334,7 +2220,7 @@ where } /// This reinserts any blocks in the new chain that do not already exist in the tree - fn reinsert_reorged_blocks(&mut self, new_chain: Vec>) { + fn reinsert_reorged_blocks(&mut self, new_chain: Vec>) { for block in new_chain { if self .state @@ -2505,11 +2391,7 @@ where &mut self, block_id: BlockWithParent, input: Input, - execute: impl FnOnce( - &mut V, - Input, - TreeCtx<'_, N>, - ) -> Result, Err>, + execute: impl FnOnce(&mut V, Input, TreeCtx<'_, N>) -> Result, Err>, convert_to_block: impl FnOnce(&mut Self, Input) -> Result, Err>, ) -> Result where @@ -2604,109 +2486,6 @@ where Ok(InsertPayloadOk::Inserted(BlockStatus::Valid)) } - /// Computes the trie input at the provided parent hash. - /// - /// The goal of this function is to take in-memory blocks and generate a [`TrieInput`] that - /// serves as an overlay to the database blocks. - /// - /// It works as follows: - /// 1. Collect in-memory blocks that are descendants of the provided parent hash using - /// [`TreeState::blocks_by_hash`]. - /// 2. If the persistence is in progress, and the block that we're computing the trie input for - /// is a descendant of the currently persisting blocks, we need to be sure that in-memory - /// blocks are not overlapping with the database blocks that may have been already persisted. - /// To do that, we're filtering out in-memory blocks that are lower than the highest database - /// block. - /// 3. Once in-memory blocks are collected and optionally filtered, we compute the - /// [`HashedPostState`] from them. - fn compute_trie_input( - &self, - persisting_kind: PersistingKind, - provider: TP, - parent_hash: B256, - allocated_trie_input: Option, - ) -> ProviderResult { - // get allocated trie input or use a default trie input - let mut input = allocated_trie_input.unwrap_or_default(); - - let best_block_number = provider.best_block_number()?; - - let (mut historical, mut blocks) = self - .state - .tree_state - .blocks_by_hash(parent_hash) - .map_or_else(|| (parent_hash.into(), vec![]), |(hash, blocks)| (hash.into(), blocks)); - - // If the current block is a descendant of the currently persisting blocks, then we need to - // filter in-memory blocks, so that none of them are already persisted in the database. - if persisting_kind.is_descendant() { - // Iterate over the blocks from oldest to newest. - while let Some(block) = blocks.last() { - let recovered_block = block.recovered_block(); - if recovered_block.number() <= best_block_number { - // Remove those blocks that lower than or equal to the highest database - // block. - blocks.pop(); - } else { - // If the block is higher than the best block number, stop filtering, as it's - // the first block that's not in the database. - break - } - } - - historical = if let Some(block) = blocks.last() { - // If there are any in-memory blocks left after filtering, set the anchor to the - // parent of the oldest block. - (block.recovered_block().number() - 1).into() - } else { - // Otherwise, set the anchor to the original provided parent hash. - parent_hash.into() - }; - } - - if blocks.is_empty() { - debug!(target: "engine::tree", %parent_hash, "Parent found on disk"); - } else { - debug!(target: "engine::tree", %parent_hash, %historical, blocks = blocks.len(), "Parent found in memory"); - } - - // Convert the historical block to the block number. - let block_number = provider - .convert_hash_or_number(historical)? - .ok_or_else(|| ProviderError::BlockHashNotFound(historical.as_hash().unwrap()))?; - - // Retrieve revert state for historical block. - let revert_state = if block_number == best_block_number { - // We do not check against the `last_block_number` here because - // `HashedPostState::from_reverts` only uses the database tables, and not static files. - debug!(target: "engine::tree", block_number, best_block_number, "Empty revert state"); - HashedPostState::default() - } else { - let revert_state = HashedPostState::from_reverts::( - provider.tx_ref(), - block_number + 1.., - ) - .map_err(ProviderError::from)?; - debug!( - target: "engine::tree", - block_number, - best_block_number, - accounts = revert_state.accounts.len(), - storages = revert_state.storages.len(), - "Non-empty revert state" - ); - revert_state - }; - input.append(revert_state); - - // Extend with contents of parent in-memory blocks. - input.extend_with_blocks( - blocks.iter().rev().map(|block| (block.hashed_state(), block.trie_updates())), - ); - - Ok(input) - } - /// Handles an error that occurred while inserting a block. /// /// If this is a validation error this will mark the block as invalid. diff --git a/crates/engine/tree/src/tree/payload_validator.rs b/crates/engine/tree/src/tree/payload_validator.rs index 17dc511a445..4e73747ef74 100644 --- a/crates/engine/tree/src/tree/payload_validator.rs +++ b/crates/engine/tree/src/tree/payload_validator.rs @@ -16,9 +16,7 @@ use alloy_consensus::transaction::Either; use alloy_eips::{eip1898::BlockWithParent, NumHash}; use alloy_evm::Evm; use alloy_primitives::B256; -use reth_chain_state::{ - CanonicalInMemoryState, ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates, -}; +use reth_chain_state::{CanonicalInMemoryState, ExecutedBlock}; use reth_consensus::{ConsensusError, FullConsensus}; use reth_engine_primitives::{ ConfigureEngineEvm, ExecutableTxIterator, ExecutionPayload, InvalidBlockHook, PayloadValidator, @@ -35,12 +33,15 @@ use reth_primitives_traits::{ AlloyBlockHeader, BlockTy, GotExpected, NodePrimitives, RecoveredBlock, SealedHeader, }; use reth_provider::{ - BlockExecutionOutput, BlockHashReader, BlockNumReader, BlockReader, DBProvider, - DatabaseProviderFactory, ExecutionOutcome, HashedPostStateProvider, HeaderProvider, - ProviderError, StateProvider, StateProviderFactory, StateReader, StateRootProvider, + BlockExecutionOutput, BlockNumReader, BlockReader, DBProvider, DatabaseProviderFactory, + ExecutionOutcome, HashedPostStateProvider, ProviderError, StateProvider, StateProviderFactory, + StateReader, StateRootProvider, TrieReader, }; use reth_revm::db::State; -use reth_trie::{updates::TrieUpdates, HashedPostState, KeccakKeyHasher, TrieInput}; +use reth_trie::{ + updates::{TrieUpdates, TrieUpdatesSorted}, + HashedPostState, KeccakKeyHasher, TrieInput, +}; use reth_trie_db::DatabaseHashedPostState; use reth_trie_parallel::root::{ParallelStateRoot, ParallelStateRootError}; use std::{collections::HashMap, sync::Arc, time::Instant}; @@ -166,7 +167,7 @@ where impl BasicEngineValidator where N: NodePrimitives, - P: DatabaseProviderFactory + P: DatabaseProviderFactory + BlockReader

+ StateProviderFactory + StateReader @@ -282,7 +283,7 @@ where input: BlockOrPayload, execution_err: InsertBlockErrorKind, parent_block: &SealedHeader, - ) -> Result, InsertPayloadError> + ) -> Result, InsertPayloadError> where V: PayloadValidator, { @@ -395,15 +396,12 @@ where // Plan the strategy used for state root computation. let state_root_plan = self.plan_state_root_computation(&input, &ctx); let persisting_kind = state_root_plan.persisting_kind; - let has_ancestors_with_missing_trie_updates = - state_root_plan.has_ancestors_with_missing_trie_updates; let strategy = state_root_plan.strategy; debug!( target: "engine::tree", block=?block_num_hash, ?strategy, - ?has_ancestors_with_missing_trie_updates, "Deciding which state root algorithm to run" ); @@ -560,38 +558,11 @@ where // terminate prewarming task with good state output handle.terminate_caching(Some(&output.state)); - // If the block doesn't connect to the database tip, we don't save its trie updates, because - // they may be incorrect as they were calculated on top of the forked block. - // - // We also only save trie updates if all ancestors have trie updates, because otherwise the - // trie updates may be incorrect. - // - // Instead, they will be recomputed on persistence. - let connects_to_last_persisted = - ensure_ok_post_block!(self.block_connects_to_last_persisted(ctx, &block), block); - let should_discard_trie_updates = - !connects_to_last_persisted || has_ancestors_with_missing_trie_updates; - debug!( - target: "engine::tree", - block = ?block_num_hash, - connects_to_last_persisted, - has_ancestors_with_missing_trie_updates, - should_discard_trie_updates, - "Checking if should discard trie updates" - ); - let trie_updates = if should_discard_trie_updates { - ExecutedTrieUpdates::Missing - } else { - ExecutedTrieUpdates::Present(Arc::new(trie_output)) - }; - - Ok(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(block), - execution_output: Arc::new(ExecutionOutcome::from((output, block_num_hash.number))), - hashed_state: Arc::new(hashed_state), - }, - trie: trie_updates, + Ok(ExecutedBlock { + recovered_block: Arc::new(block), + execution_output: Arc::new(ExecutionOutcome::from((output, block_num_hash.number))), + hashed_state: Arc::new(hashed_state), + trie_updates: Arc::new(trie_output), }) } @@ -719,51 +690,6 @@ where ParallelStateRoot::new(consistent_view, input).incremental_root_with_updates() } - /// Checks if the given block connects to the last persisted block, i.e. if the last persisted - /// block is the ancestor of the given block. - /// - /// This checks the database for the actual last persisted block, not [`PersistenceState`]. - fn block_connects_to_last_persisted( - &self, - ctx: TreeCtx<'_, N>, - block: &RecoveredBlock, - ) -> ProviderResult { - let provider = self.provider.database_provider_ro()?; - let last_persisted_block = provider.best_block_number()?; - let last_persisted_hash = provider - .block_hash(last_persisted_block)? - .ok_or(ProviderError::HeaderNotFound(last_persisted_block.into()))?; - let last_persisted = NumHash::new(last_persisted_block, last_persisted_hash); - - let parent_num_hash = |hash: B256| -> ProviderResult { - let parent_num_hash = - if let Some(header) = ctx.state().tree_state.sealed_header_by_hash(&hash) { - Some(header.parent_num_hash()) - } else { - provider.sealed_header_by_hash(hash)?.map(|header| header.parent_num_hash()) - }; - - parent_num_hash.ok_or(ProviderError::BlockHashNotFound(hash)) - }; - - let mut parent_block = block.parent_num_hash(); - while parent_block.number > last_persisted.number { - parent_block = parent_num_hash(parent_block.hash)?; - } - - let connects = parent_block == last_persisted; - - debug!( - target: "engine::tree", - num_hash = ?block.num_hash(), - ?last_persisted, - ?parent_block, - "Checking if block connects to last persisted block" - ); - - Ok(connects) - } - /// Validates the block after execution. /// /// This performs: @@ -947,27 +873,6 @@ where } } - /// Check if the given block has any ancestors with missing trie updates. - fn has_ancestors_with_missing_trie_updates( - &self, - target_header: BlockWithParent, - state: &EngineApiTreeState, - ) -> bool { - // Walk back through the chain starting from the parent of the target block - let mut current_hash = target_header.parent; - while let Some(block) = state.tree_state.blocks_by_hash.get(¤t_hash) { - // Check if this block is missing trie updates - if block.trie.is_missing() { - return true; - } - - // Move to the parent block - current_hash = block.recovered_block().parent_hash(); - } - - false - } - /// Creates a `StateProviderBuilder` for the given parent hash. /// /// This method checks if the parent is in the tree state (in-memory) or persisted to disk, @@ -1018,20 +923,12 @@ where let can_run_parallel = persisting_kind.can_run_parallel_state_root() && !self.config.state_root_fallback(); - // Check for ancestors with missing trie updates - let has_ancestors_with_missing_trie_updates = - self.has_ancestors_with_missing_trie_updates(input.block_with_parent(), ctx.state()); - // Decide on the strategy. // Use state root task only if: // 1. No persistence is in progress // 2. Config allows it - // 3. No ancestors with missing trie updates. If any exist, it will mean that every state - // root task proof calculation will include a lot of unrelated paths in the prefix sets. - // It's cheaper to run a parallel state root that does one walk over trie tables while - // accounting for the prefix sets. let strategy = if can_run_parallel { - if self.config.use_state_root_task() && !has_ancestors_with_missing_trie_updates { + if self.config.use_state_root_task() { StateRootStrategy::StateRootTask } else { StateRootStrategy::Parallel @@ -1044,11 +941,10 @@ where target: "engine::tree", block=?input.num_hash(), ?strategy, - has_ancestors_with_missing_trie_updates, "Planned state root computation strategy" ); - StateRootPlan { strategy, has_ancestors_with_missing_trie_updates, persisting_kind } + StateRootPlan { strategy, persisting_kind } } /// Called when an invalid block is encountered during validation. @@ -1082,7 +978,7 @@ where /// block. /// 3. Once in-memory blocks are collected and optionally filtered, we compute the /// [`HashedPostState`] from them. - fn compute_trie_input( + fn compute_trie_input( &self, persisting_kind: PersistingKind, provider: TP, @@ -1139,17 +1035,19 @@ where .ok_or_else(|| ProviderError::BlockHashNotFound(historical.as_hash().unwrap()))?; // Retrieve revert state for historical block. - let revert_state = if block_number == best_block_number { + let (revert_state, revert_trie) = if block_number == best_block_number { // We do not check against the `last_block_number` here because - // `HashedPostState::from_reverts` only uses the database tables, and not static files. + // `HashedPostState::from_reverts` / `trie_reverts` only use the database tables, and + // not static files. debug!(target: "engine::tree", block_number, best_block_number, "Empty revert state"); - HashedPostState::default() + (HashedPostState::default(), TrieUpdatesSorted::default()) } else { let revert_state = HashedPostState::from_reverts::( provider.tx_ref(), block_number + 1.., ) .map_err(ProviderError::from)?; + let revert_trie = provider.trie_reverts(block_number + 1)?; debug!( target: "engine::tree", block_number, @@ -1158,9 +1056,10 @@ where storages = revert_state.storages.len(), "Non-empty revert state" ); - revert_state + (revert_state, revert_trie) }; - input.append(revert_state); + + input.append_cached(revert_trie.into(), revert_state); // Extend with contents of parent in-memory blocks. input.extend_with_blocks( @@ -1172,8 +1071,7 @@ where } /// Output of block or payload validation. -pub type ValidationOutcome>> = - Result, E>; +pub type ValidationOutcome>> = Result, E>; /// Strategy describing how to compute the state root. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -1190,8 +1088,6 @@ enum StateRootStrategy { struct StateRootPlan { /// Strategy that should be attempted for computing the state root. strategy: StateRootStrategy, - /// Whether ancestors have missing trie updates. - has_ancestors_with_missing_trie_updates: bool, /// The persisting kind for this block. persisting_kind: PersistingKind, } @@ -1249,7 +1145,7 @@ pub trait EngineValidator< impl EngineValidator for BasicEngineValidator where - P: DatabaseProviderFactory + P: DatabaseProviderFactory + BlockReader
+ StateProviderFactory + StateReader diff --git a/crates/engine/tree/src/tree/state.rs b/crates/engine/tree/src/tree/state.rs index cab7d35fb22..f38faf6524c 100644 --- a/crates/engine/tree/src/tree/state.rs +++ b/crates/engine/tree/src/tree/state.rs @@ -1,29 +1,19 @@ //! Functionality related to tree state. use crate::engine::EngineApiKind; -use alloy_eips::{eip1898::BlockWithParent, merge::EPOCH_SLOTS, BlockNumHash}; +use alloy_eips::{eip1898::BlockWithParent, BlockNumHash}; use alloy_primitives::{ map::{HashMap, HashSet}, BlockNumber, B256, }; -use reth_chain_state::{EthPrimitives, ExecutedBlockWithTrieUpdates}; +use reth_chain_state::{EthPrimitives, ExecutedBlock}; use reth_primitives_traits::{AlloyBlockHeader, NodePrimitives, SealedHeader}; -use reth_trie::updates::TrieUpdates; use std::{ collections::{btree_map, hash_map, BTreeMap, VecDeque}, ops::Bound, - sync::Arc, }; use tracing::debug; -/// Default number of blocks to retain persisted trie updates -const DEFAULT_PERSISTED_TRIE_UPDATES_RETENTION: u64 = EPOCH_SLOTS * 2; - -/// Number of blocks to retain persisted trie updates for OP Stack chains -/// OP Stack chains only need `EPOCH_SLOTS` as reorgs are relevant only when -/// op-node reorgs to the same chain twice -const OPSTACK_PERSISTED_TRIE_UPDATES_RETENTION: u64 = EPOCH_SLOTS; - /// Keeps track of the state of the tree. /// /// ## Invariants @@ -35,19 +25,15 @@ pub struct TreeState { /// __All__ unique executed blocks by block hash that are connected to the canonical chain. /// /// This includes blocks of all forks. - pub(crate) blocks_by_hash: HashMap>, + pub(crate) blocks_by_hash: HashMap>, /// Executed blocks grouped by their respective block number. /// /// This maps unique block number to all known blocks for that height. /// /// Note: there can be multiple blocks at the same height due to forks. - pub(crate) blocks_by_number: BTreeMap>>, + pub(crate) blocks_by_number: BTreeMap>>, /// Map of any parent block hash to its children. pub(crate) parent_to_child: HashMap>, - /// Map of hash to trie updates for canonical blocks that are persisted but not finalized. - /// - /// Contains the block number for easy removal. - pub(crate) persisted_trie_updates: HashMap)>, /// Currently tracked canonical head of the chain. pub(crate) current_canonical_head: BlockNumHash, /// The engine API variant of this handler @@ -62,7 +48,6 @@ impl TreeState { blocks_by_number: BTreeMap::new(), current_canonical_head, parent_to_child: HashMap::default(), - persisted_trie_updates: HashMap::default(), engine_kind, } } @@ -77,11 +62,8 @@ impl TreeState { self.blocks_by_hash.len() } - /// Returns the [`ExecutedBlockWithTrieUpdates`] by hash. - pub(crate) fn executed_block_by_hash( - &self, - hash: B256, - ) -> Option<&ExecutedBlockWithTrieUpdates> { + /// Returns the [`ExecutedBlock`] by hash. + pub(crate) fn executed_block_by_hash(&self, hash: B256) -> Option<&ExecutedBlock> { self.blocks_by_hash.get(&hash) } @@ -97,10 +79,7 @@ impl TreeState { /// newest to oldest. And the parent hash of the oldest block that is missing from the buffer. /// /// Returns `None` if the block for the given hash is not found. - pub(crate) fn blocks_by_hash( - &self, - hash: B256, - ) -> Option<(B256, Vec>)> { + pub(crate) fn blocks_by_hash(&self, hash: B256) -> Option<(B256, Vec>)> { let block = self.blocks_by_hash.get(&hash).cloned()?; let mut parent_hash = block.recovered_block().parent_hash(); let mut blocks = vec![block]; @@ -113,7 +92,7 @@ impl TreeState { } /// Insert executed block into the state. - pub(crate) fn insert_executed(&mut self, executed: ExecutedBlockWithTrieUpdates) { + pub(crate) fn insert_executed(&mut self, executed: ExecutedBlock) { let hash = executed.recovered_block().hash(); let parent_hash = executed.recovered_block().parent_hash(); let block_number = executed.recovered_block().number(); @@ -138,10 +117,7 @@ impl TreeState { /// ## Returns /// /// The removed block and the block hashes of its children. - fn remove_by_hash( - &mut self, - hash: B256, - ) -> Option<(ExecutedBlockWithTrieUpdates, HashSet)> { + fn remove_by_hash(&mut self, hash: B256) -> Option<(ExecutedBlock, HashSet)> { let executed = self.blocks_by_hash.remove(&hash)?; // Remove this block from collection of children of its parent block. @@ -215,41 +191,12 @@ impl TreeState { if executed.recovered_block().number() <= upper_bound { let num_hash = executed.recovered_block().num_hash(); debug!(target: "engine::tree", ?num_hash, "Attempting to remove block walking back from the head"); - if let Some((mut removed, _)) = - self.remove_by_hash(executed.recovered_block().hash()) - { - debug!(target: "engine::tree", ?num_hash, "Removed block walking back from the head"); - // finally, move the trie updates - let Some(trie_updates) = removed.trie.take_present() else { - debug!(target: "engine::tree", ?num_hash, "No trie updates found for persisted block"); - continue; - }; - self.persisted_trie_updates.insert( - removed.recovered_block().hash(), - (removed.recovered_block().number(), trie_updates), - ); - } + self.remove_by_hash(executed.recovered_block().hash()); } } debug!(target: "engine::tree", ?upper_bound, ?last_persisted_hash, "Removed canonical blocks from the tree"); } - /// Prunes old persisted trie updates based on the current block number - /// and chain type (OP Stack or regular) - pub(crate) fn prune_persisted_trie_updates(&mut self) { - let retention_blocks = if self.engine_kind.is_opstack() { - OPSTACK_PERSISTED_TRIE_UPDATES_RETENTION - } else { - DEFAULT_PERSISTED_TRIE_UPDATES_RETENTION - }; - - let earliest_block_to_retain = - self.current_canonical_head.number.saturating_sub(retention_blocks); - - self.persisted_trie_updates - .retain(|_, (block_number, _)| *block_number > earliest_block_to_retain); - } - /// Removes all blocks that are below the finalized block, as well as removing non-canonical /// sidechains that fork from below the finalized block. pub(crate) fn prune_finalized_sidechains(&mut self, finalized_num_hash: BlockNumHash) { @@ -274,8 +221,6 @@ impl TreeState { } } - self.prune_persisted_trie_updates(); - // The only block that should remain at the `finalized` number now, is the finalized // block, if it exists. // diff --git a/crates/engine/tree/src/tree/tests.rs b/crates/engine/tree/src/tree/tests.rs index 17b5950e077..49ce5ab9cf1 100644 --- a/crates/engine/tree/src/tree/tests.rs +++ b/crates/engine/tree/src/tree/tests.rs @@ -3,6 +3,7 @@ use crate::{ persistence::PersistenceAction, tree::{ payload_validator::{BasicEngineValidator, TreeCtx, ValidationOutcome}, + persistence_state::CurrentPersistenceAction, TreeConfig, }, }; @@ -26,7 +27,7 @@ use reth_ethereum_primitives::{Block, EthPrimitives}; use reth_evm_ethereum::MockEvmConfig; use reth_primitives_traits::Block as _; use reth_provider::{test_utils::MockEthProvider, ExecutionOutcome}; -use reth_trie::HashedPostState; +use reth_trie::{updates::TrieUpdates, HashedPostState}; use std::{ collections::BTreeMap, str::FromStr, @@ -148,7 +149,7 @@ struct TestHarness { >, to_tree_tx: Sender, Block>>, from_tree_rx: UnboundedReceiver, - blocks: Vec, + blocks: Vec, action_rx: Receiver, block_builder: TestBlockBuilder, provider: MockEthProvider, @@ -228,7 +229,7 @@ impl TestHarness { } } - fn with_blocks(mut self, blocks: Vec) -> Self { + fn with_blocks(mut self, blocks: Vec) -> Self { let mut blocks_by_hash = HashMap::default(); let mut blocks_by_number = BTreeMap::new(); let mut state_by_hash = HashMap::default(); @@ -253,7 +254,6 @@ impl TestHarness { blocks_by_number, current_canonical_head: blocks.last().unwrap().recovered_block().num_hash(), parent_to_child, - persisted_trie_updates: HashMap::default(), engine_kind: EngineApiKind::Ethereum, }; @@ -405,7 +405,6 @@ impl ValidatorTestHarness { /// Configure `PersistenceState` for specific `PersistingKind` scenarios fn start_persistence_operation(&mut self, action: CurrentPersistenceAction) { - use crate::tree::persistence_state::CurrentPersistenceAction; use tokio::sync::oneshot; // Create a dummy receiver for testing - it will never receive a value @@ -828,25 +827,21 @@ fn test_tree_state_on_new_head_deep_fork() { let chain_b = test_block_builder.create_fork(&last_block, 10); for block in &chain_a { - test_harness.tree.state.tree_state.insert_executed(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(block.clone()), - execution_output: Arc::new(ExecutionOutcome::default()), - hashed_state: Arc::new(HashedPostState::default()), - }, - trie: ExecutedTrieUpdates::empty(), + test_harness.tree.state.tree_state.insert_executed(ExecutedBlock { + recovered_block: Arc::new(block.clone()), + execution_output: Arc::new(ExecutionOutcome::default()), + hashed_state: Arc::new(HashedPostState::default()), + trie_updates: Arc::new(TrieUpdates::default()), }); } test_harness.tree.state.tree_state.set_canonical_head(chain_a.last().unwrap().num_hash()); for block in &chain_b { - test_harness.tree.state.tree_state.insert_executed(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(block.clone()), - execution_output: Arc::new(ExecutionOutcome::default()), - hashed_state: Arc::new(HashedPostState::default()), - }, - trie: ExecutedTrieUpdates::empty(), + test_harness.tree.state.tree_state.insert_executed(ExecutedBlock { + recovered_block: Arc::new(block.clone()), + execution_output: Arc::new(ExecutionOutcome::default()), + hashed_state: Arc::new(HashedPostState::default()), + trie_updates: Arc::new(TrieUpdates::default()), }); } diff --git a/crates/exex/exex/src/backfill/factory.rs b/crates/exex/exex/src/backfill/factory.rs index 789d63f84e2..d9a51bc47a7 100644 --- a/crates/exex/exex/src/backfill/factory.rs +++ b/crates/exex/exex/src/backfill/factory.rs @@ -24,7 +24,7 @@ impl BackfillJobFactory { Self { evm_config, provider, - prune_modes: PruneModes::none(), + prune_modes: PruneModes::default(), thresholds: ExecutionStageThresholds { // Default duration for a database transaction to be considered long-lived is // 60 seconds, so we limit the backfill job to the half of it to be sure we finish diff --git a/crates/node/core/src/args/pruning.rs b/crates/node/core/src/args/pruning.rs index e96245350fd..846e4e6b203 100644 --- a/crates/node/core/src/args/pruning.rs +++ b/crates/node/core/src/args/pruning.rs @@ -126,6 +126,7 @@ impl PruningArgs { storage_history: Some(PruneMode::Distance(MINIMUM_PRUNING_DISTANCE)), // TODO: set default to pre-merge block if available bodies_history: None, + merkle_changesets: PruneMode::Distance(MINIMUM_PRUNING_DISTANCE), receipts_log_filter: Default::default(), }, } diff --git a/crates/node/core/src/args/stage.rs b/crates/node/core/src/args/stage.rs index 337f5a4a60b..7718fb85605 100644 --- a/crates/node/core/src/args/stage.rs +++ b/crates/node/core/src/args/stage.rs @@ -38,6 +38,11 @@ pub enum StageEnum { /// /// Handles Merkle tree-related computations and data processing. Merkle, + /// The merkle changesets stage within the pipeline. + /// + /// Handles Merkle trie changesets for storage and accounts. + #[value(name = "merkle-changesets")] + MerkleChangeSets, /// The transaction lookup stage within the pipeline. /// /// Deals with the retrieval and processing of transactions. diff --git a/crates/optimism/flashblocks/src/worker.rs b/crates/optimism/flashblocks/src/worker.rs index 68071851f43..8cf7777f6a6 100644 --- a/crates/optimism/flashblocks/src/worker.rs +++ b/crates/optimism/flashblocks/src/worker.rs @@ -124,6 +124,7 @@ where recovered_block: block.into(), execution_output: Arc::new(execution_outcome), hashed_state: Arc::new(hashed_state), + trie_updates: Arc::default(), }, ); let pending_flashblock = PendingFlashBlock::new( diff --git a/crates/optimism/payload/src/builder.rs b/crates/optimism/payload/src/builder.rs index 1d73464e178..fbd351424f7 100644 --- a/crates/optimism/payload/src/builder.rs +++ b/crates/optimism/payload/src/builder.rs @@ -11,7 +11,7 @@ use alloy_primitives::{B256, U256}; use alloy_rpc_types_debug::ExecutionWitness; use alloy_rpc_types_engine::PayloadId; use reth_basic_payload_builder::*; -use reth_chain_state::{ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates}; +use reth_chain_state::ExecutedBlock; use reth_chainspec::{ChainSpecProvider, EthChainSpec}; use reth_evm::{ execute::{ @@ -379,13 +379,11 @@ impl OpBuilder<'_, Txs> { ); // create the executed block data - let executed: ExecutedBlockWithTrieUpdates = ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(block), - execution_output: Arc::new(execution_outcome), - hashed_state: Arc::new(hashed_state), - }, - trie: ExecutedTrieUpdates::Present(Arc::new(trie_updates)), + let executed: ExecutedBlock = ExecutedBlock { + recovered_block: Arc::new(block), + execution_output: Arc::new(execution_outcome), + hashed_state: Arc::new(hashed_state), + trie_updates: Arc::new(trie_updates), }; let no_tx_pool = ctx.attributes().no_tx_pool(); diff --git a/crates/optimism/payload/src/payload.rs b/crates/optimism/payload/src/payload.rs index de1705faa8f..6f530acd853 100644 --- a/crates/optimism/payload/src/payload.rs +++ b/crates/optimism/payload/src/payload.rs @@ -16,7 +16,7 @@ use op_alloy_consensus::{encode_holocene_extra_data, encode_jovian_extra_data, E use op_alloy_rpc_types_engine::{ OpExecutionPayloadEnvelopeV3, OpExecutionPayloadEnvelopeV4, OpExecutionPayloadV4, }; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_chainspec::EthChainSpec; use reth_optimism_evm::OpNextBlockEnvAttributes; use reth_optimism_forks::OpHardforks; @@ -176,7 +176,7 @@ pub struct OpBuiltPayload { /// Sealed block pub(crate) block: Arc>, /// Block execution data for the payload, if any. - pub(crate) executed_block: Option>, + pub(crate) executed_block: Option>, /// The fees of the block pub(crate) fees: U256, } @@ -189,7 +189,7 @@ impl OpBuiltPayload { id: PayloadId, block: Arc>, fees: U256, - executed_block: Option>, + executed_block: Option>, ) -> Self { Self { id, block, fees, executed_block } } @@ -226,7 +226,7 @@ impl BuiltPayload for OpBuiltPayload { self.fees } - fn executed_block(&self) -> Option> { + fn executed_block(&self) -> Option> { self.executed_block.clone() } diff --git a/crates/payload/primitives/src/traits.rs b/crates/payload/primitives/src/traits.rs index 39bd14cc63b..160956afa27 100644 --- a/crates/payload/primitives/src/traits.rs +++ b/crates/payload/primitives/src/traits.rs @@ -9,7 +9,7 @@ use alloy_eips::{ use alloy_primitives::{Address, B256, U256}; use alloy_rpc_types_engine::{PayloadAttributes as EthPayloadAttributes, PayloadId}; use core::fmt; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_primitives_traits::{NodePrimitives, SealedBlock, SealedHeader}; /// Represents a successfully built execution payload (block). @@ -30,7 +30,7 @@ pub trait BuiltPayload: Send + Sync + fmt::Debug { /// Returns the complete execution result including state updates. /// /// Returns `None` if execution data is not available or not tracked. - fn executed_block(&self) -> Option> { + fn executed_block(&self) -> Option> { None } diff --git a/crates/prune/prune/src/builder.rs b/crates/prune/prune/src/builder.rs index 1987c500da7..f21319bb458 100644 --- a/crates/prune/prune/src/builder.rs +++ b/crates/prune/prune/src/builder.rs @@ -6,8 +6,8 @@ use reth_db_api::{table::Value, transaction::DbTxMut}; use reth_exex_types::FinishedExExHeight; use reth_primitives_traits::NodePrimitives; use reth_provider::{ - providers::StaticFileProvider, BlockReader, DBProvider, DatabaseProviderFactory, - NodePrimitivesProvider, PruneCheckpointReader, PruneCheckpointWriter, + providers::StaticFileProvider, BlockReader, ChainStateBlockReader, DBProvider, + DatabaseProviderFactory, NodePrimitivesProvider, PruneCheckpointReader, PruneCheckpointWriter, StaticFileProviderFactory, }; use reth_prune_types::PruneModes; @@ -83,6 +83,7 @@ impl PrunerBuilder { ProviderRW: PruneCheckpointWriter + PruneCheckpointReader + BlockReader + + ChainStateBlockReader + StaticFileProviderFactory< Primitives: NodePrimitives, >, @@ -113,6 +114,7 @@ impl PrunerBuilder { Primitives: NodePrimitives, > + DBProvider + BlockReader + + ChainStateBlockReader + PruneCheckpointWriter + PruneCheckpointReader, { @@ -132,7 +134,7 @@ impl Default for PrunerBuilder { fn default() -> Self { Self { block_interval: 5, - segments: PruneModes::none(), + segments: PruneModes::default(), delete_limit: MAINNET_PRUNE_DELETE_LIMIT, timeout: None, finished_exex_height: watch::channel(FinishedExExHeight::NoExExs).1, diff --git a/crates/prune/prune/src/segments/mod.rs b/crates/prune/prune/src/segments/mod.rs index 1daade01358..dc175254453 100644 --- a/crates/prune/prune/src/segments/mod.rs +++ b/crates/prune/prune/src/segments/mod.rs @@ -15,8 +15,8 @@ pub use static_file::{ use std::{fmt::Debug, ops::RangeInclusive}; use tracing::error; pub use user::{ - AccountHistory, Receipts as UserReceipts, ReceiptsByLogs, SenderRecovery, StorageHistory, - TransactionLookup, + AccountHistory, MerkleChangeSets, Receipts as UserReceipts, ReceiptsByLogs, SenderRecovery, + StorageHistory, TransactionLookup, }; /// A segment represents a pruning of some portion of the data. diff --git a/crates/prune/prune/src/segments/set.rs b/crates/prune/prune/src/segments/set.rs index 08e41bcdf75..72847219b09 100644 --- a/crates/prune/prune/src/segments/set.rs +++ b/crates/prune/prune/src/segments/set.rs @@ -1,13 +1,13 @@ use crate::segments::{ - AccountHistory, ReceiptsByLogs, Segment, SenderRecovery, StorageHistory, TransactionLookup, - UserReceipts, + AccountHistory, MerkleChangeSets, ReceiptsByLogs, Segment, SenderRecovery, StorageHistory, + TransactionLookup, UserReceipts, }; use alloy_eips::eip2718::Encodable2718; use reth_db_api::{table::Value, transaction::DbTxMut}; use reth_primitives_traits::NodePrimitives; use reth_provider::{ - providers::StaticFileProvider, BlockReader, DBProvider, PruneCheckpointReader, - PruneCheckpointWriter, StaticFileProviderFactory, + providers::StaticFileProvider, BlockReader, ChainStateBlockReader, DBProvider, + PruneCheckpointReader, PruneCheckpointWriter, StaticFileProviderFactory, }; use reth_prune_types::PruneModes; @@ -52,7 +52,8 @@ where > + DBProvider + PruneCheckpointWriter + PruneCheckpointReader - + BlockReader, + + BlockReader + + ChainStateBlockReader, { /// Creates a [`SegmentSet`] from an existing components, such as [`StaticFileProvider`] and /// [`PruneModes`]. @@ -67,6 +68,7 @@ where account_history, storage_history, bodies_history: _, + merkle_changesets, receipts_log_filter, } = prune_modes; @@ -77,6 +79,8 @@ where .segment(StaticFileTransactions::new(static_file_provider.clone())) // Static file receipts .segment(StaticFileReceipts::new(static_file_provider)) + // Merkle changesets + .segment(MerkleChangeSets::new(merkle_changesets)) // Account history .segment_opt(account_history.map(AccountHistory::new)) // Storage history diff --git a/crates/prune/prune/src/segments/user/merkle_change_sets.rs b/crates/prune/prune/src/segments/user/merkle_change_sets.rs new file mode 100644 index 00000000000..89cc4567b7d --- /dev/null +++ b/crates/prune/prune/src/segments/user/merkle_change_sets.rs @@ -0,0 +1,116 @@ +use crate::{ + db_ext::DbTxPruneExt, + segments::{PruneInput, Segment}, + PrunerError, +}; +use alloy_primitives::B256; +use reth_db_api::{models::BlockNumberHashedAddress, table::Value, tables, transaction::DbTxMut}; +use reth_primitives_traits::NodePrimitives; +use reth_provider::{ + errors::provider::ProviderResult, BlockReader, ChainStateBlockReader, DBProvider, + NodePrimitivesProvider, PruneCheckpointWriter, TransactionsProvider, +}; +use reth_prune_types::{ + PruneCheckpoint, PruneMode, PrunePurpose, PruneSegment, SegmentOutput, SegmentOutputCheckpoint, +}; +use tracing::{instrument, trace}; + +#[derive(Debug)] +pub struct MerkleChangeSets { + mode: PruneMode, +} + +impl MerkleChangeSets { + pub const fn new(mode: PruneMode) -> Self { + Self { mode } + } +} + +impl Segment for MerkleChangeSets +where + Provider: DBProvider + + PruneCheckpointWriter + + TransactionsProvider + + BlockReader + + ChainStateBlockReader + + NodePrimitivesProvider>, +{ + fn segment(&self) -> PruneSegment { + PruneSegment::MerkleChangeSets + } + + fn mode(&self) -> Option { + Some(self.mode) + } + + fn purpose(&self) -> PrunePurpose { + PrunePurpose::User + } + + #[instrument(level = "trace", target = "pruner", skip(self, provider), ret)] + fn prune(&self, provider: &Provider, input: PruneInput) -> Result { + let Some(block_range) = input.get_next_block_range() else { + trace!(target: "pruner", "No change sets to prune"); + return Ok(SegmentOutput::done()) + }; + + let block_range_end = *block_range.end(); + let mut limiter = input.limiter; + + // Create range for StoragesTrieChangeSets which uses BlockNumberHashedAddress as key + let storage_range_start: BlockNumberHashedAddress = + (*block_range.start(), B256::ZERO).into(); + let storage_range_end: BlockNumberHashedAddress = + (*block_range.end() + 1, B256::ZERO).into(); + let storage_range = storage_range_start..storage_range_end; + + let mut last_storages_pruned_block = None; + let (storages_pruned, done) = + provider.tx_ref().prune_table_with_range::( + storage_range, + &mut limiter, + |_| false, + |(BlockNumberHashedAddress((block_number, _)), _)| { + last_storages_pruned_block = Some(block_number); + }, + )?; + + trace!(target: "pruner", %storages_pruned, %done, "Pruned storages change sets"); + + let mut last_accounts_pruned_block = block_range_end; + let last_storages_pruned_block = last_storages_pruned_block + // If there's more storage changesets to prune, set the checkpoint block number to + // previous, so we could finish pruning its storage changesets on the next run. + .map(|block_number| if done { block_number } else { block_number.saturating_sub(1) }) + .unwrap_or(block_range_end); + + let (accounts_pruned, done) = + provider.tx_ref().prune_table_with_range::( + block_range, + &mut limiter, + |_| false, + |row| last_accounts_pruned_block = row.0, + )?; + + trace!(target: "pruner", %accounts_pruned, %done, "Pruned accounts change sets"); + + let progress = limiter.progress(done); + + Ok(SegmentOutput { + progress, + pruned: accounts_pruned + storages_pruned, + checkpoint: Some(SegmentOutputCheckpoint { + block_number: Some(last_storages_pruned_block.min(last_accounts_pruned_block)), + tx_number: None, + }), + }) + } + + fn save_checkpoint( + &self, + provider: &Provider, + checkpoint: PruneCheckpoint, + ) -> ProviderResult<()> { + provider.save_prune_checkpoint(PruneSegment::MerkleChangeSets, checkpoint) + } +} diff --git a/crates/prune/prune/src/segments/user/mod.rs b/crates/prune/prune/src/segments/user/mod.rs index 0b787d14dae..c25bc6bc764 100644 --- a/crates/prune/prune/src/segments/user/mod.rs +++ b/crates/prune/prune/src/segments/user/mod.rs @@ -1,5 +1,6 @@ mod account_history; mod history; +mod merkle_change_sets; mod receipts; mod receipts_by_logs; mod sender_recovery; @@ -7,6 +8,7 @@ mod storage_history; mod transaction_lookup; pub use account_history::AccountHistory; +pub use merkle_change_sets::MerkleChangeSets; pub use receipts::Receipts; pub use receipts_by_logs::ReceiptsByLogs; pub use sender_recovery::SenderRecovery; diff --git a/crates/prune/types/src/segment.rs b/crates/prune/types/src/segment.rs index e131f353fe3..0d60d900137 100644 --- a/crates/prune/types/src/segment.rs +++ b/crates/prune/types/src/segment.rs @@ -3,6 +3,9 @@ use derive_more::Display; use thiserror::Error; /// Segment of the data that can be pruned. +/// +/// NOTE new variants must be added to the end of this enum. The variant index is encoded directly +/// when writing to the `PruneCheckpoint` table, so changing the order here will corrupt the table. #[derive(Debug, Display, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(test, derive(arbitrary::Arbitrary))] #[cfg_attr(any(test, feature = "reth-codec"), derive(reth_codecs::Compact))] @@ -26,6 +29,9 @@ pub enum PruneSegment { Headers, /// Prune segment responsible for the `Transactions` table. Transactions, + /// Prune segment responsible for all rows in `AccountsTrieChangeSets` and + /// `StoragesTrieChangeSets` table. + MerkleChangeSets, } #[cfg(test)] @@ -44,9 +50,10 @@ impl PruneSegment { 0 } Self::Receipts if purpose.is_static_file() => 0, - Self::ContractLogs | Self::AccountHistory | Self::StorageHistory => { - MINIMUM_PRUNING_DISTANCE - } + Self::ContractLogs | + Self::AccountHistory | + Self::StorageHistory | + Self::MerkleChangeSets | Self::Receipts => MINIMUM_PRUNING_DISTANCE, } } diff --git a/crates/prune/types/src/target.rs b/crates/prune/types/src/target.rs index 574a0e2e555..657cf6a37c5 100644 --- a/crates/prune/types/src/target.rs +++ b/crates/prune/types/src/target.rs @@ -36,8 +36,13 @@ pub enum HistoryType { StorageHistory, } +/// Default pruning mode for merkle changesets +const fn default_merkle_changesets_mode() -> PruneMode { + PruneMode::Distance(MINIMUM_PRUNING_DISTANCE) +} + /// Pruning configuration for every segment of the data that can be pruned. -#[derive(Debug, Clone, Default, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] #[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(any(test, feature = "serde"), serde(default))] pub struct PruneModes { @@ -84,6 +89,16 @@ pub struct PruneModes { ) )] pub bodies_history: Option, + /// Merkle Changesets pruning configuration for `AccountsTrieChangeSets` and + /// `StoragesTrieChangeSets`. + #[cfg_attr( + any(test, feature = "serde"), + serde( + default = "default_merkle_changesets_mode", + deserialize_with = "deserialize_prune_mode_with_min_blocks::" + ) + )] + pub merkle_changesets: PruneMode, /// Receipts pruning configuration by retaining only those receipts that contain logs emitted /// by the specified addresses, discarding others. This setting is overridden by `receipts`. /// @@ -92,12 +107,22 @@ pub struct PruneModes { pub receipts_log_filter: ReceiptsLogPruneConfig, } -impl PruneModes { - /// Sets pruning to no target. - pub fn none() -> Self { - Self::default() +impl Default for PruneModes { + fn default() -> Self { + Self { + sender_recovery: None, + transaction_lookup: None, + receipts: None, + account_history: None, + storage_history: None, + bodies_history: None, + merkle_changesets: default_merkle_changesets_mode(), + receipts_log_filter: ReceiptsLogPruneConfig::default(), + } } +} +impl PruneModes { /// Sets pruning to all targets. pub fn all() -> Self { Self { @@ -107,6 +132,7 @@ impl PruneModes { account_history: Some(PruneMode::Full), storage_history: Some(PruneMode::Full), bodies_history: Some(PruneMode::Full), + merkle_changesets: PruneMode::Full, receipts_log_filter: Default::default(), } } @@ -116,11 +142,6 @@ impl PruneModes { self.receipts.is_some() || !self.receipts_log_filter.is_empty() } - /// Returns true if all prune modes are set to [`None`]. - pub fn is_empty(&self) -> bool { - self == &Self::none() - } - /// Returns an error if we can't unwind to the targeted block because the target block is /// outside the range. /// @@ -170,6 +191,28 @@ impl PruneModes { } } +/// Deserializes [`PruneMode`] and validates that the value is not less than the const +/// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be +/// left in database after the pruning. +/// +/// 1. For [`PruneMode::Full`], it fails if `MIN_BLOCKS > 0`. +/// 2. For [`PruneMode::Distance`], it fails if `distance < MIN_BLOCKS + 1`. `+ 1` is needed because +/// `PruneMode::Distance(0)` means that we leave zero blocks from the latest, meaning we have one +/// block in the database. +#[cfg(any(test, feature = "serde"))] +fn deserialize_prune_mode_with_min_blocks< + 'de, + const MIN_BLOCKS: u64, + D: serde::Deserializer<'de>, +>( + deserializer: D, +) -> Result { + use serde::Deserialize; + let prune_mode = PruneMode::deserialize(deserializer)?; + serde_deserialize_validate::(&prune_mode)?; + Ok(prune_mode) +} + /// Deserializes [`Option`] and validates that the value is not less than the const /// generic parameter `MIN_BLOCKS`. This parameter represents the number of blocks that needs to be /// left in database after the pruning. @@ -186,12 +229,21 @@ fn deserialize_opt_prune_mode_with_min_blocks< >( deserializer: D, ) -> Result, D::Error> { - use alloc::format; use serde::Deserialize; let prune_mode = Option::::deserialize(deserializer)?; + if let Some(prune_mode) = prune_mode.as_ref() { + serde_deserialize_validate::(prune_mode)?; + } + Ok(prune_mode) +} +#[cfg(any(test, feature = "serde"))] +fn serde_deserialize_validate<'a, 'de, const MIN_BLOCKS: u64, D: serde::Deserializer<'de>>( + prune_mode: &'a PruneMode, +) -> Result<(), D::Error> { + use alloc::format; match prune_mode { - Some(PruneMode::Full) if MIN_BLOCKS > 0 => { + PruneMode::Full if MIN_BLOCKS > 0 => { Err(serde::de::Error::invalid_value( serde::de::Unexpected::Str("full"), // This message should have "expected" wording @@ -199,15 +251,15 @@ fn deserialize_opt_prune_mode_with_min_blocks< .as_str(), )) } - Some(PruneMode::Distance(distance)) if distance < MIN_BLOCKS => { + PruneMode::Distance(distance) if *distance < MIN_BLOCKS => { Err(serde::de::Error::invalid_value( - serde::de::Unexpected::Unsigned(distance), + serde::de::Unexpected::Unsigned(*distance), // This message should have "expected" wording &format!("prune mode that leaves at least {MIN_BLOCKS} blocks in the database") .as_str(), )) } - _ => Ok(prune_mode), + _ => Ok(()), } } @@ -240,7 +292,7 @@ mod tests { #[test] fn test_unwind_target_unpruned() { // Test case 1: No pruning configured - should always succeed - let prune_modes = PruneModes::none(); + let prune_modes = PruneModes::default(); assert!(prune_modes.ensure_unwind_target_unpruned(1000, 500, &[]).is_ok()); assert!(prune_modes.ensure_unwind_target_unpruned(1000, 0, &[]).is_ok()); diff --git a/crates/ress/provider/src/lib.rs b/crates/ress/provider/src/lib.rs index 599b37962f0..d986eb9e953 100644 --- a/crates/ress/provider/src/lib.rs +++ b/crates/ress/provider/src/lib.rs @@ -11,9 +11,7 @@ use alloy_consensus::BlockHeader as _; use alloy_primitives::{Bytes, B256}; use parking_lot::Mutex; -use reth_chain_state::{ - ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates, MemoryOverlayStateProvider, -}; +use reth_chain_state::{ExecutedBlock, MemoryOverlayStateProvider}; use reth_errors::{ProviderError, ProviderResult}; use reth_ethereum_primitives::{Block, BlockBody, EthPrimitives}; use reth_evm::{execute::Executor, ConfigureEvm}; @@ -125,10 +123,8 @@ where self.pending_state.invalid_recovered_block(&ancestor_hash) { trace!(target: "reth::ress_provider", %block_hash, %ancestor_hash, "Using invalid ancestor block for witness construction"); - executed = Some(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { recovered_block: invalid, ..Default::default() }, - trie: ExecutedTrieUpdates::empty(), - }); + executed = + Some(ExecutedBlock { recovered_block: invalid, ..Default::default() }); } let Some(executed) = executed else { @@ -162,14 +158,8 @@ where let witness_state_provider = self.provider.state_by_block_hash(ancestor_hash)?; let mut trie_input = TrieInput::default(); for block in executed_ancestors.into_iter().rev() { - if let Some(trie_updates) = block.trie.as_ref() { - trie_input.append_cached_ref(trie_updates, &block.hashed_state); - } else { - trace!(target: "reth::ress_provider", ancestor = ?block.recovered_block().num_hash(), "Missing trie updates for ancestor block"); - return Err(ProviderError::TrieWitnessError( - "missing trie updates for ancestor".to_owned(), - )); - } + let trie_updates = block.trie_updates.as_ref(); + trie_input.append_cached_ref(trie_updates, &block.hashed_state); } let mut hashed_state = db.into_state(); hashed_state.extend(record.hashed_state); diff --git a/crates/ress/provider/src/pending_state.rs b/crates/ress/provider/src/pending_state.rs index e1a84661fc2..f536acdb60a 100644 --- a/crates/ress/provider/src/pending_state.rs +++ b/crates/ress/provider/src/pending_state.rs @@ -5,7 +5,7 @@ use alloy_primitives::{ }; use futures::StreamExt; use parking_lot::RwLock; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_ethereum_primitives::EthPrimitives; use reth_node_api::{ConsensusEngineEvent, NodePrimitives}; use reth_primitives_traits::{Bytecode, RecoveredBlock}; @@ -20,14 +20,14 @@ pub struct PendingState(Arc>>); #[derive(Default, Debug)] struct PendingStateInner { - blocks_by_hash: B256Map>, + blocks_by_hash: B256Map>, invalid_blocks_by_hash: B256Map>>, block_hashes_by_number: BTreeMap, } impl PendingState { /// Insert executed block with trie updates. - pub fn insert_block(&self, block: ExecutedBlockWithTrieUpdates) { + pub fn insert_block(&self, block: ExecutedBlock) { let mut this = self.0.write(); let block_hash = block.recovered_block.hash(); this.block_hashes_by_number @@ -46,13 +46,13 @@ impl PendingState { } /// Returns only valid executed blocks by hash. - pub fn executed_block(&self, hash: &B256) -> Option> { + pub fn executed_block(&self, hash: &B256) -> Option> { self.0.read().blocks_by_hash.get(hash).cloned() } /// Returns valid recovered block. pub fn recovered_block(&self, hash: &B256) -> Option>> { - self.executed_block(hash).map(|b| b.recovered_block.clone()) + self.executed_block(hash).map(|b| b.recovered_block) } /// Returns invalid recovered block. diff --git a/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs b/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs index 94dc214b6c8..06a20d0ca20 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/pending_block.rs @@ -369,7 +369,7 @@ pub trait LoadPendingBlock: } } - let BlockBuilderOutcome { execution_result, block, hashed_state, .. } = + let BlockBuilderOutcome { execution_result, block, hashed_state, trie_updates } = builder.finish(NoopProvider::default()).map_err(Self::Error::from_eth_err)?; let execution_outcome = ExecutionOutcome::new( @@ -383,6 +383,7 @@ pub trait LoadPendingBlock: recovered_block: block.into(), execution_output: Arc::new(execution_outcome), hashed_state: Arc::new(hashed_state), + trie_updates: Arc::new(trie_updates), }) } } diff --git a/crates/rpc/rpc-eth-types/src/pending_block.rs b/crates/rpc/rpc-eth-types/src/pending_block.rs index 05ad6fb4e27..c2507bd66e4 100644 --- a/crates/rpc/rpc-eth-types/src/pending_block.rs +++ b/crates/rpc/rpc-eth-types/src/pending_block.rs @@ -9,9 +9,7 @@ use alloy_consensus::BlockHeader; use alloy_eips::{BlockId, BlockNumberOrTag}; use alloy_primitives::{BlockHash, B256}; use derive_more::Constructor; -use reth_chain_state::{ - BlockState, ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates, -}; +use reth_chain_state::{BlockState, ExecutedBlock}; use reth_ethereum_primitives::Receipt; use reth_evm::EvmEnv; use reth_primitives_traits::{ @@ -135,11 +133,6 @@ impl PendingBlock { impl From> for BlockState { fn from(pending_block: PendingBlock) -> Self { - Self::new(ExecutedBlockWithTrieUpdates::::new( - pending_block.executed_block.recovered_block, - pending_block.executed_block.execution_output, - pending_block.executed_block.hashed_state, - ExecutedTrieUpdates::Missing, - )) + Self::new(pending_block.executed_block) } } diff --git a/crates/stages/api/src/pipeline/mod.rs b/crates/stages/api/src/pipeline/mod.rs index 2446219ea3d..ac35a489031 100644 --- a/crates/stages/api/src/pipeline/mod.rs +++ b/crates/stages/api/src/pipeline/mod.rs @@ -639,14 +639,18 @@ impl Pipeline { // FIXME: When handling errors, we do not commit the database transaction. This // leads to the Merkle stage not clearing its checkpoint, and restarting from an // invalid place. - let provider_rw = self.provider_factory.database_provider_rw()?; - provider_rw.save_stage_checkpoint_progress(StageId::MerkleExecute, vec![])?; - provider_rw.save_stage_checkpoint( - StageId::MerkleExecute, - prev_checkpoint.unwrap_or_default(), - )?; + // Only reset MerkleExecute checkpoint if MerkleExecute itself failed + if stage_id == StageId::MerkleExecute { + let provider_rw = self.provider_factory.database_provider_rw()?; + provider_rw + .save_stage_checkpoint_progress(StageId::MerkleExecute, vec![])?; + provider_rw.save_stage_checkpoint( + StageId::MerkleExecute, + prev_checkpoint.unwrap_or_default(), + )?; - provider_rw.commit()?; + provider_rw.commit()?; + } // We unwind because of a validation error. If the unwind itself // fails, we bail entirely, diff --git a/crates/stages/stages/benches/setup/mod.rs b/crates/stages/stages/benches/setup/mod.rs index bd1fb59ebe9..01d7571e0da 100644 --- a/crates/stages/stages/benches/setup/mod.rs +++ b/crates/stages/stages/benches/setup/mod.rs @@ -165,7 +165,7 @@ pub(crate) fn txs_testdata(num_blocks: u64) -> TestStageDB { db.insert_changesets(transitions, None).unwrap(); let provider_rw = db.factory.provider_rw().unwrap(); - provider_rw.write_trie_updates(&updates).unwrap(); + provider_rw.write_trie_updates(updates).unwrap(); provider_rw.commit().unwrap(); let (transitions, final_state) = random_changeset_range( diff --git a/crates/stages/stages/src/sets.rs b/crates/stages/stages/src/sets.rs index 97c3a3116aa..015be507336 100644 --- a/crates/stages/stages/src/sets.rs +++ b/crates/stages/stages/src/sets.rs @@ -39,9 +39,9 @@ use crate::{ stages::{ AccountHashingStage, BodyStage, EraImportSource, EraStage, ExecutionStage, FinishStage, - HeaderStage, IndexAccountHistoryStage, IndexStorageHistoryStage, MerkleStage, - PruneSenderRecoveryStage, PruneStage, SenderRecoveryStage, StorageHashingStage, - TransactionLookupStage, + HeaderStage, IndexAccountHistoryStage, IndexStorageHistoryStage, MerkleChangeSets, + MerkleStage, PruneSenderRecoveryStage, PruneStage, SenderRecoveryStage, + StorageHashingStage, TransactionLookupStage, }, StageSet, StageSetBuilder, }; @@ -54,7 +54,7 @@ use reth_primitives_traits::{Block, NodePrimitives}; use reth_provider::HeaderSyncGapProvider; use reth_prune_types::PruneModes; use reth_stages_api::Stage; -use std::{ops::Not, sync::Arc}; +use std::sync::Arc; use tokio::sync::watch; /// A set containing all stages to run a fully syncing instance of reth. @@ -75,6 +75,7 @@ use tokio::sync::watch; /// - [`AccountHashingStage`] /// - [`StorageHashingStage`] /// - [`MerkleStage`] (execute) +/// - [`MerkleChangeSets`] /// - [`TransactionLookupStage`] /// - [`IndexStorageHistoryStage`] /// - [`IndexAccountHistoryStage`] @@ -336,12 +337,12 @@ where stages_config: self.stages_config.clone(), prune_modes: self.prune_modes.clone(), }) - // If any prune modes are set, add the prune stage. - .add_stage_opt(self.prune_modes.is_empty().not().then(|| { - // Prune stage should be added after all hashing stages, because otherwise it will - // delete - PruneStage::new(self.prune_modes.clone(), self.stages_config.prune.commit_threshold) - })) + // Prune stage should be added after all hashing stages, because otherwise it will + // delete + .add_stage(PruneStage::new( + self.prune_modes.clone(), + self.stages_config.prune.commit_threshold, + )) } } @@ -387,6 +388,13 @@ where } /// A set containing all stages that hash account state. +/// +/// This includes: +/// - [`MerkleStage`] (unwind) +/// - [`AccountHashingStage`] +/// - [`StorageHashingStage`] +/// - [`MerkleStage`] (execute) +/// - [`MerkleChangeSets`] #[derive(Debug, Default)] #[non_exhaustive] pub struct HashingStages { @@ -399,6 +407,7 @@ where MerkleStage: Stage, AccountHashingStage: Stage, StorageHashingStage: Stage, + MerkleChangeSets: Stage, { fn builder(self) -> StageSetBuilder { StageSetBuilder::default() @@ -415,6 +424,7 @@ where self.stages_config.merkle.rebuild_threshold, self.stages_config.merkle.incremental_threshold, )) + .add_stage(MerkleChangeSets::new()) } } diff --git a/crates/stages/stages/src/stages/execution.rs b/crates/stages/stages/src/stages/execution.rs index 3736fa523cb..ed50572d58b 100644 --- a/crates/stages/stages/src/stages/execution.rs +++ b/crates/stages/stages/src/stages/execution.rs @@ -896,7 +896,7 @@ mod tests { // If there is a pruning configuration, then it's forced to use the database. // This way we test both cases. - let modes = [None, Some(PruneModes::none())]; + let modes = [None, Some(PruneModes::default())]; let random_filter = ReceiptsLogPruneConfig(BTreeMap::from([( Address::random(), PruneMode::Distance(100000), @@ -1033,7 +1033,7 @@ mod tests { // If there is a pruning configuration, then it's forced to use the database. // This way we test both cases. - let modes = [None, Some(PruneModes::none())]; + let modes = [None, Some(PruneModes::default())]; let random_filter = ReceiptsLogPruneConfig(BTreeMap::from([( Address::random(), PruneMode::Before(100000), diff --git a/crates/stages/stages/src/stages/merkle.rs b/crates/stages/stages/src/stages/merkle.rs index 6cbed3ab20e..b4f24db7c58 100644 --- a/crates/stages/stages/src/stages/merkle.rs +++ b/crates/stages/stages/src/stages/merkle.rs @@ -247,7 +247,7 @@ where })?; match progress { StateRootProgress::Progress(state, hashed_entries_walked, updates) => { - provider.write_trie_updates(&updates)?; + provider.write_trie_updates(updates)?; let mut checkpoint = MerkleCheckpoint::new( to_block, @@ -290,7 +290,7 @@ where }) } StateRootProgress::Complete(root, hashed_entries_walked, updates) => { - provider.write_trie_updates(&updates)?; + provider.write_trie_updates(updates)?; entities_checkpoint.processed += hashed_entries_walked as u64; @@ -317,7 +317,7 @@ where error!(target: "sync::stages::merkle", %e, ?current_block_number, ?to_block, "Incremental state root failed! {INVALID_STATE_ROOT_ERROR_MESSAGE}"); StageError::Fatal(Box::new(e)) })?; - provider.write_trie_updates(&updates)?; + provider.write_trie_updates(updates)?; final_root = Some(root); } @@ -400,7 +400,7 @@ where validate_state_root(block_root, SealedHeader::seal_slow(target), input.unwind_to)?; // Validation passed, apply unwind changes to the database. - provider.write_trie_updates(&updates)?; + provider.write_trie_updates(updates)?; // Update entities checkpoint to reflect the unwind operation // Since we're unwinding, we need to recalculate the total entities at the target block diff --git a/crates/stages/stages/src/stages/merkle_changesets.rs b/crates/stages/stages/src/stages/merkle_changesets.rs new file mode 100644 index 00000000000..7bf756c3dd3 --- /dev/null +++ b/crates/stages/stages/src/stages/merkle_changesets.rs @@ -0,0 +1,380 @@ +use crate::stages::merkle::INVALID_STATE_ROOT_ERROR_MESSAGE; +use alloy_consensus::BlockHeader; +use alloy_primitives::BlockNumber; +use reth_consensus::ConsensusError; +use reth_primitives_traits::{GotExpected, SealedHeader}; +use reth_provider::{ + ChainStateBlockReader, DBProvider, HeaderProvider, ProviderError, StageCheckpointReader, + TrieWriter, +}; +use reth_stages_api::{ + BlockErrorKind, CheckpointBlockRange, ExecInput, ExecOutput, MerkleChangeSetsCheckpoint, Stage, + StageCheckpoint, StageError, StageId, UnwindInput, UnwindOutput, +}; +use reth_trie::{updates::TrieUpdates, HashedPostState, KeccakKeyHasher, StateRoot, TrieInput}; +use reth_trie_db::{DatabaseHashedPostState, DatabaseStateRoot}; +use std::ops::Range; +use tracing::{debug, error}; + +/// The `MerkleChangeSets` stage. +/// +/// This stage processes and maintains trie changesets from the finalized block to the latest block. +#[derive(Debug, Clone)] +pub struct MerkleChangeSets { + /// The number of blocks to retain changesets for, used as a fallback when the finalized block + /// is not found. Defaults to 64 (2 epochs in beacon chain). + retention_blocks: u64, +} + +impl MerkleChangeSets { + /// Creates a new `MerkleChangeSets` stage with default retention blocks of 64. + pub const fn new() -> Self { + Self { retention_blocks: 64 } + } + + /// Creates a new `MerkleChangeSets` stage with a custom finalized block height. + pub const fn with_retention_blocks(retention_blocks: u64) -> Self { + Self { retention_blocks } + } + + /// Returns the range of blocks which are already computed. Will return an empty range if none + /// have been computed. + fn computed_range(checkpoint: Option) -> Range { + let to = checkpoint.map(|chk| chk.block_number).unwrap_or_default(); + let from = checkpoint + .map(|chk| chk.merkle_changesets_stage_checkpoint().unwrap_or_default()) + .unwrap_or_default() + .block_range + .to; + from..to + 1 + } + + /// Determines the target range for changeset computation based on the checkpoint and provider + /// state. + /// + /// Returns the target range (exclusive end) to compute changesets for. + fn determine_target_range( + &self, + provider: &Provider, + ) -> Result, StageError> + where + Provider: StageCheckpointReader + ChainStateBlockReader, + { + // Get merkle checkpoint which represents our target end block + let merkle_checkpoint = provider + .get_stage_checkpoint(StageId::MerkleExecute)? + .map(|checkpoint| checkpoint.block_number) + .unwrap_or(0); + + let target_end = merkle_checkpoint + 1; // exclusive + + // Calculate the target range based on the finalized block and the target block. + // We maintain changesets from the finalized block to the latest block. + let finalized_block = provider.last_finalized_block_number()?; + + // Calculate the fallback start position based on retention blocks + let retention_based_start = merkle_checkpoint.saturating_sub(self.retention_blocks); + + // If the finalized block was way in the past then we don't want to generate changesets for + // all of those past blocks; we only care about the recent history. + // + // Use maximum of finalized_block and retention_based_start if finalized_block exists, + // otherwise just use retention_based_start. + let mut target_start = finalized_block + .map(|finalized| finalized.saturating_add(1).max(retention_based_start)) + .unwrap_or(retention_based_start); + + // We cannot revert the genesis block; target_start must be >0 + target_start = target_start.max(1); + + Ok(target_start..target_end) + } + + /// Calculates the trie updates given a [`TrieInput`], asserting that the resulting state root + /// matches the expected one for the block. + fn calculate_block_trie_updates( + provider: &Provider, + block_number: BlockNumber, + input: TrieInput, + ) -> Result { + let (root, trie_updates) = + StateRoot::overlay_root_from_nodes_with_updates(provider.tx_ref(), input).map_err( + |e| { + error!( + target: "sync::stages::merkle_changesets", + %e, + ?block_number, + "Incremental state root failed! {INVALID_STATE_ROOT_ERROR_MESSAGE}"); + StageError::Fatal(Box::new(e)) + }, + )?; + + let block = provider + .header_by_number(block_number)? + .ok_or_else(|| ProviderError::HeaderNotFound(block_number.into()))?; + + let (got, expected) = (root, block.state_root()); + if got != expected { + // Only seal the header when we need it for the error + let header = SealedHeader::seal_slow(block); + error!( + target: "sync::stages::merkle_changesets", + ?block_number, + ?got, + ?expected, + "Failed to verify block state root! {INVALID_STATE_ROOT_ERROR_MESSAGE}", + ); + return Err(StageError::Block { + error: BlockErrorKind::Validation(ConsensusError::BodyStateRootDiff( + GotExpected { got, expected }.into(), + )), + block: Box::new(header.block_with_parent()), + }) + } + + Ok(trie_updates) + } + + fn populate_range( + provider: &Provider, + target_range: Range, + ) -> Result<(), StageError> + where + Provider: StageCheckpointReader + + TrieWriter + + DBProvider + + HeaderProvider + + ChainStateBlockReader, + { + let target_start = target_range.start; + let target_end = target_range.end; + debug!( + target: "sync::stages::merkle_changesets", + ?target_range, + "Starting trie changeset computation", + ); + + // We need to distinguish a cumulative revert and a per-block revert. A cumulative revert + // reverts changes starting at db tip all the way to a block. A per-block revert only + // reverts a block's changes. + // + // We need to calculate the cumulative HashedPostState reverts for every block in the + // target range. The cumulative HashedPostState revert for block N can be calculated as: + // + // + // ``` + // // where `extend` overwrites any shared keys + // cumulative_state_revert(N) = cumulative_state_revert(N + 1).extend(get_block_state_revert(N)) + // ``` + // + // We need per-block reverts to calculate the prefix set for each individual block. By + // using the per-block reverts to calculate cumulative reverts on-the-fly we can save a + // bunch of memory. + debug!( + target: "sync::stages::merkle_changesets", + ?target_range, + "Computing per-block state reverts", + ); + let mut per_block_state_reverts = Vec::new(); + for block_number in target_range.clone() { + per_block_state_reverts.push(HashedPostState::from_reverts::( + provider.tx_ref(), + block_number..=block_number, + )?); + } + + // Helper to retrieve state revert data for a specific block from the pre-computed array + let get_block_state_revert = |block_number: BlockNumber| -> &HashedPostState { + let index = (block_number - target_start) as usize; + &per_block_state_reverts[index] + }; + + // Helper to accumulate state reverts from a given block to the target end + let compute_cumulative_state_revert = |block_number: BlockNumber| -> HashedPostState { + let mut cumulative_revert = HashedPostState::default(); + for n in (block_number..target_end).rev() { + cumulative_revert.extend_ref(get_block_state_revert(n)) + } + cumulative_revert + }; + + // To calculate the changeset for a block, we first need the TrieUpdates which are + // generated as a result of processing the block. To get these we need: + // 1) The TrieUpdates which revert the db's trie to _prior_ to the block + // 2) The HashedPostState to revert the db's state to _after_ the block + // + // To get (1) for `target_start` we need to do a big state root calculation which takes + // into account all changes between that block and db tip. For each block after the + // `target_start` we can update (1) using the TrieUpdates which were output by the previous + // block, only targeting the state changes of that block. + debug!( + target: "sync::stages::merkle_changesets", + ?target_start, + "Computing trie state at starting block", + ); + let mut input = TrieInput::default(); + input.state = compute_cumulative_state_revert(target_start); + input.prefix_sets = input.state.construct_prefix_sets(); + // target_start will be >= 1, see `determine_target_range`. + input.nodes = + Self::calculate_block_trie_updates(provider, target_start - 1, input.clone())?; + + for block_number in target_range { + debug!( + target: "sync::stages::merkle_changesets", + ?block_number, + "Computing trie updates for block", + ); + // Revert the state so that this block has been just processed, meaning we take the + // cumulative revert of the subsequent block. + input.state = compute_cumulative_state_revert(block_number + 1); + + // Construct prefix sets from only this block's `HashedPostState`, because we only care + // about trie updates which occurred as a result of this block being processed. + input.prefix_sets = get_block_state_revert(block_number).construct_prefix_sets(); + + // Calculate the trie updates for this block, then apply those updates to the reverts. + // We calculate the overlay which will be passed into the next step using the trie + // reverts prior to them being updated. + let this_trie_updates = + Self::calculate_block_trie_updates(provider, block_number, input.clone())?; + + let trie_overlay = input.nodes.clone().into_sorted(); + input.nodes.extend_ref(&this_trie_updates); + let this_trie_updates = this_trie_updates.into_sorted(); + + // Write the changesets to the DB using the trie updates produced by the block, and the + // trie reverts as the overlay. + debug!( + target: "sync::stages::merkle_changesets", + ?block_number, + "Writing trie changesets for block", + ); + provider.write_trie_changesets( + block_number, + &this_trie_updates, + Some(&trie_overlay), + )?; + } + + Ok(()) + } +} + +impl Default for MerkleChangeSets { + fn default() -> Self { + Self::new() + } +} + +impl Stage for MerkleChangeSets +where + Provider: + StageCheckpointReader + TrieWriter + DBProvider + HeaderProvider + ChainStateBlockReader, +{ + fn id(&self) -> StageId { + StageId::MerkleChangeSets + } + + fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result { + // Get merkle checkpoint and assert that the target is the same. + let merkle_checkpoint = provider + .get_stage_checkpoint(StageId::MerkleExecute)? + .map(|checkpoint| checkpoint.block_number) + .unwrap_or(0); + + if input.target.is_none_or(|target| merkle_checkpoint != target) { + return Err(StageError::Fatal(eyre::eyre!("Cannot sync stage to block {:?} when MerkleExecute is at block {merkle_checkpoint:?}", input.target).into())) + } + + let mut target_range = self.determine_target_range(provider)?; + + // Get the previously computed range. This will be updated to reflect the populating of the + // target range. + let mut computed_range = Self::computed_range(input.checkpoint); + + // We want the target range to not include any data already computed previously, if + // possible, so we start the target range from the end of the computed range if that is + // greater. + // + // ------------------------------> Block # + // |------computed-----| + // |-----target-----| + // |--actual--| + // + // However, if the target start is less than the previously computed start, we don't want to + // do this, as it would leave a gap of data at `target_range.start..=computed_range.start`. + // + // ------------------------------> Block # + // |---computed---| + // |-------target-------| + // |-------actual-------| + // + if target_range.start >= computed_range.start { + target_range.start = target_range.start.max(computed_range.end); + } + + // If target range is empty (target_start >= target_end), stage is already successfully + // executed + if target_range.start >= target_range.end { + return Ok(ExecOutput::done(input.checkpoint.unwrap_or_default())); + } + + // If our target range is a continuation of the already computed range then we can keep the + // already computed data. + if target_range.start == computed_range.end { + // Clear from target_start onwards to ensure no stale data exists + provider.clear_trie_changesets_from(target_range.start)?; + computed_range.end = target_range.end; + } else { + // If our target range is not a continuation of the already computed range then we + // simply clear the computed data, to make sure there's no gaps or conflicts. + provider.clear_trie_changesets()?; + computed_range = target_range.clone(); + } + + // Populate the target range with changesets + Self::populate_range(provider, target_range)?; + + let checkpoint_block_range = CheckpointBlockRange { + from: computed_range.start, + // CheckpointBlockRange is inclusive + to: computed_range.end.saturating_sub(1), + }; + + let checkpoint = StageCheckpoint::new(checkpoint_block_range.to) + .with_merkle_changesets_stage_checkpoint(MerkleChangeSetsCheckpoint { + block_range: checkpoint_block_range, + }); + + Ok(ExecOutput::done(checkpoint)) + } + + fn unwind( + &mut self, + provider: &Provider, + input: UnwindInput, + ) -> Result { + // Unwinding is trivial; just clear everything after the target block. + provider.clear_trie_changesets_from(input.unwind_to + 1)?; + + let mut computed_range = Self::computed_range(Some(input.checkpoint)); + computed_range.end = input.unwind_to + 1; + if computed_range.start > computed_range.end { + computed_range.start = computed_range.end; + } + + let checkpoint_block_range = CheckpointBlockRange { + from: computed_range.start, + // computed_range.end is exclusive + to: computed_range.end.saturating_sub(1), + }; + + let checkpoint = StageCheckpoint::new(input.unwind_to) + .with_merkle_changesets_stage_checkpoint(MerkleChangeSetsCheckpoint { + block_range: checkpoint_block_range, + }); + + Ok(UnwindOutput { checkpoint }) + } +} diff --git a/crates/stages/stages/src/stages/mod.rs b/crates/stages/stages/src/stages/mod.rs index 7e57009e808..40c4cb91368 100644 --- a/crates/stages/stages/src/stages/mod.rs +++ b/crates/stages/stages/src/stages/mod.rs @@ -16,6 +16,8 @@ mod index_account_history; mod index_storage_history; /// Stage for computing state root. mod merkle; +/// Stage for computing merkle changesets. +mod merkle_changesets; mod prune; /// The sender recovery stage. mod sender_recovery; @@ -32,6 +34,7 @@ pub use headers::*; pub use index_account_history::*; pub use index_storage_history::*; pub use merkle::*; +pub use merkle_changesets::*; pub use prune::*; pub use sender_recovery::*; pub use tx_lookup::*; @@ -223,7 +226,7 @@ mod tests { // In an unpruned configuration there is 1 receipt, 3 changed accounts and 1 changed // storage. - let mut prune = PruneModes::none(); + let mut prune = PruneModes::default(); check_pruning(test_db.factory.clone(), prune.clone(), 1, 3, 1).await; prune.receipts = Some(PruneMode::Full); diff --git a/crates/stages/stages/src/stages/prune.rs b/crates/stages/stages/src/stages/prune.rs index f62259dcfdd..3161d4b1412 100644 --- a/crates/stages/stages/src/stages/prune.rs +++ b/crates/stages/stages/src/stages/prune.rs @@ -1,7 +1,7 @@ use reth_db_api::{table::Value, transaction::DbTxMut}; use reth_primitives_traits::NodePrimitives; use reth_provider::{ - BlockReader, DBProvider, PruneCheckpointReader, PruneCheckpointWriter, + BlockReader, ChainStateBlockReader, DBProvider, PruneCheckpointReader, PruneCheckpointWriter, StaticFileProviderFactory, }; use reth_prune::{ @@ -42,6 +42,7 @@ where + PruneCheckpointReader + PruneCheckpointWriter + BlockReader + + ChainStateBlockReader + StaticFileProviderFactory< Primitives: NodePrimitives, >, @@ -121,7 +122,7 @@ impl PruneSenderRecoveryStage { /// Create new prune sender recovery stage with the given prune mode and commit threshold. pub fn new(prune_mode: PruneMode, commit_threshold: usize) -> Self { Self(PruneStage::new( - PruneModes { sender_recovery: Some(prune_mode), ..PruneModes::none() }, + PruneModes { sender_recovery: Some(prune_mode), ..PruneModes::default() }, commit_threshold, )) } @@ -133,6 +134,7 @@ where + PruneCheckpointReader + PruneCheckpointWriter + BlockReader + + ChainStateBlockReader + StaticFileProviderFactory< Primitives: NodePrimitives, >, diff --git a/crates/stages/types/src/checkpoints.rs b/crates/stages/types/src/checkpoints.rs index 61c399d9ac3..16bee1387f6 100644 --- a/crates/stages/types/src/checkpoints.rs +++ b/crates/stages/types/src/checkpoints.rs @@ -287,6 +287,17 @@ pub struct IndexHistoryCheckpoint { pub progress: EntitiesCheckpoint, } +/// Saves the progress of `MerkleChangeSets` stage. +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "test-utils"), derive(arbitrary::Arbitrary))] +#[cfg_attr(any(test, feature = "reth-codec"), derive(reth_codecs::Compact))] +#[cfg_attr(any(test, feature = "reth-codec"), reth_codecs::add_arbitrary_tests(compact))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct MerkleChangeSetsCheckpoint { + /// Block range which this checkpoint is valid for. + pub block_range: CheckpointBlockRange, +} + /// Saves the progress of abstract stage iterating over or downloading entities. #[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] #[cfg_attr(any(test, feature = "test-utils"), derive(arbitrary::Arbitrary))] @@ -386,6 +397,9 @@ impl StageCheckpoint { StageId::IndexStorageHistory | StageId::IndexAccountHistory => { StageUnitCheckpoint::IndexHistory(IndexHistoryCheckpoint::default()) } + StageId::MerkleChangeSets => { + StageUnitCheckpoint::MerkleChangeSets(MerkleChangeSetsCheckpoint::default()) + } _ => return self, }); _ = self.stage_checkpoint.map(|mut checkpoint| checkpoint.set_block_range(from, to)); @@ -411,6 +425,7 @@ impl StageCheckpoint { progress: entities, .. }) => Some(entities), + StageUnitCheckpoint::MerkleChangeSets(_) => None, } } } @@ -436,6 +451,8 @@ pub enum StageUnitCheckpoint { Headers(HeadersCheckpoint), /// Saves the progress of Index History stage. IndexHistory(IndexHistoryCheckpoint), + /// Saves the progress of `MerkleChangeSets` stage. + MerkleChangeSets(MerkleChangeSetsCheckpoint), } impl StageUnitCheckpoint { @@ -446,7 +463,8 @@ impl StageUnitCheckpoint { Self::Account(AccountHashingCheckpoint { block_range, .. }) | Self::Storage(StorageHashingCheckpoint { block_range, .. }) | Self::Execution(ExecutionCheckpoint { block_range, .. }) | - Self::IndexHistory(IndexHistoryCheckpoint { block_range, .. }) => { + Self::IndexHistory(IndexHistoryCheckpoint { block_range, .. }) | + Self::MerkleChangeSets(MerkleChangeSetsCheckpoint { block_range, .. }) => { let old_range = *block_range; *block_range = CheckpointBlockRange { from, to }; @@ -544,6 +562,15 @@ stage_unit_checkpoints!( index_history_stage_checkpoint, /// Sets the stage checkpoint to index history. with_index_history_stage_checkpoint + ), + ( + 6, + MerkleChangeSets, + MerkleChangeSetsCheckpoint, + /// Returns the merkle changesets stage checkpoint, if any. + merkle_changesets_stage_checkpoint, + /// Sets the stage checkpoint to merkle changesets. + with_merkle_changesets_stage_checkpoint ) ); diff --git a/crates/stages/types/src/id.rs b/crates/stages/types/src/id.rs index 78d7e0ec1b6..8c0a91c8731 100644 --- a/crates/stages/types/src/id.rs +++ b/crates/stages/types/src/id.rs @@ -25,6 +25,7 @@ pub enum StageId { TransactionLookup, IndexStorageHistory, IndexAccountHistory, + MerkleChangeSets, Prune, Finish, /// Other custom stage with a provided string identifier. @@ -39,7 +40,7 @@ static ENCODED_STAGE_IDS: OnceLock>> = OnceLock::new(); impl StageId { /// All supported Stages - pub const ALL: [Self; 15] = [ + pub const ALL: [Self; 16] = [ Self::Era, Self::Headers, Self::Bodies, @@ -53,6 +54,7 @@ impl StageId { Self::TransactionLookup, Self::IndexStorageHistory, Self::IndexAccountHistory, + Self::MerkleChangeSets, Self::Prune, Self::Finish, ]; @@ -88,6 +90,7 @@ impl StageId { Self::TransactionLookup => "TransactionLookup", Self::IndexAccountHistory => "IndexAccountHistory", Self::IndexStorageHistory => "IndexStorageHistory", + Self::MerkleChangeSets => "MerkleChangeSets", Self::Prune => "Prune", Self::Finish => "Finish", Self::Other(s) => s, diff --git a/crates/stages/types/src/lib.rs b/crates/stages/types/src/lib.rs index 4e30ce27cd7..83585fee7ce 100644 --- a/crates/stages/types/src/lib.rs +++ b/crates/stages/types/src/lib.rs @@ -18,8 +18,8 @@ pub use id::StageId; mod checkpoints; pub use checkpoints::{ AccountHashingCheckpoint, CheckpointBlockRange, EntitiesCheckpoint, ExecutionCheckpoint, - HeadersCheckpoint, IndexHistoryCheckpoint, MerkleCheckpoint, StageCheckpoint, - StageUnitCheckpoint, StorageHashingCheckpoint, StorageRootMerkleCheckpoint, + HeadersCheckpoint, IndexHistoryCheckpoint, MerkleChangeSetsCheckpoint, MerkleCheckpoint, + StageCheckpoint, StageUnitCheckpoint, StorageHashingCheckpoint, StorageRootMerkleCheckpoint, }; mod execution; diff --git a/crates/storage/db-api/src/cursor.rs b/crates/storage/db-api/src/cursor.rs index 3aeee949ea1..068b64a3c97 100644 --- a/crates/storage/db-api/src/cursor.rs +++ b/crates/storage/db-api/src/cursor.rs @@ -87,7 +87,7 @@ pub trait DbDupCursorRO { /// | `key` | `subkey` | **Equivalent starting position** | /// |--------|----------|-----------------------------------------| /// | `None` | `None` | [`DbCursorRO::first()`] | - /// | `Some` | `None` | [`DbCursorRO::seek()`] | + /// | `Some` | `None` | [`DbCursorRO::seek_exact()`] | /// | `None` | `Some` | [`DbDupCursorRO::seek_by_key_subkey()`] | /// | `Some` | `Some` | [`DbDupCursorRO::seek_by_key_subkey()`] | fn walk_dup( diff --git a/crates/storage/db-api/src/models/accounts.rs b/crates/storage/db-api/src/models/accounts.rs index 263e362cc6a..41a11e1c7e5 100644 --- a/crates/storage/db-api/src/models/accounts.rs +++ b/crates/storage/db-api/src/models/accounts.rs @@ -176,7 +176,11 @@ impl Decode for AddressStorageKey { } } -impl_fixed_arbitrary!((BlockNumberAddress, 28), (AddressStorageKey, 52)); +impl_fixed_arbitrary!( + (BlockNumberAddress, 28), + (BlockNumberHashedAddress, 40), + (AddressStorageKey, 52) +); #[cfg(test)] mod tests { @@ -209,6 +213,31 @@ mod tests { assert_eq!(bytes, Encode::encode(key)); } + #[test] + fn test_block_number_hashed_address() { + let num = 1u64; + let hash = B256::from_slice(&[0xba; 32]); + let key = BlockNumberHashedAddress((num, hash)); + + let mut bytes = [0u8; 40]; + bytes[..8].copy_from_slice(&num.to_be_bytes()); + bytes[8..].copy_from_slice(hash.as_slice()); + + let encoded = Encode::encode(key); + assert_eq!(encoded, bytes); + + let decoded: BlockNumberHashedAddress = Decode::decode(&encoded).unwrap(); + assert_eq!(decoded, key); + } + + #[test] + fn test_block_number_hashed_address_rand() { + let mut bytes = [0u8; 40]; + rng().fill(bytes.as_mut_slice()); + let key = BlockNumberHashedAddress::arbitrary(&mut Unstructured::new(&bytes)).unwrap(); + assert_eq!(bytes, Encode::encode(key)); + } + #[test] fn test_address_storage_key() { let storage_key = StorageKey::random(); diff --git a/crates/storage/db-api/src/models/mod.rs b/crates/storage/db-api/src/models/mod.rs index 24951789f5d..31d9b301f8c 100644 --- a/crates/storage/db-api/src/models/mod.rs +++ b/crates/storage/db-api/src/models/mod.rs @@ -12,7 +12,9 @@ use reth_ethereum_primitives::{Receipt, TransactionSigned, TxType}; use reth_primitives_traits::{Account, Bytecode, StorageEntry}; use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::StageCheckpoint; -use reth_trie_common::{StoredNibbles, StoredNibblesSubKey, *}; +use reth_trie_common::{ + StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, TrieChangeSetsEntry, *, +}; use serde::{Deserialize, Serialize}; pub mod accounts; @@ -219,6 +221,7 @@ impl_compression_for_compact!( TxType, StorageEntry, BranchNodeCompact, + TrieChangeSetsEntry, StoredNibbles, StoredNibblesSubKey, StorageTrieEntry, diff --git a/crates/storage/db-api/src/tables/mod.rs b/crates/storage/db-api/src/tables/mod.rs index 259b2d39b15..cd678260128 100644 --- a/crates/storage/db-api/src/tables/mod.rs +++ b/crates/storage/db-api/src/tables/mod.rs @@ -21,8 +21,8 @@ use crate::{ accounts::BlockNumberAddress, blocks::{HeaderHash, StoredBlockOmmers}, storage_sharded_key::StorageShardedKey, - AccountBeforeTx, ClientVersion, CompactU256, IntegerList, ShardedKey, - StoredBlockBodyIndices, StoredBlockWithdrawals, + AccountBeforeTx, BlockNumberHashedAddress, ClientVersion, CompactU256, IntegerList, + ShardedKey, StoredBlockBodyIndices, StoredBlockWithdrawals, }, table::{Decode, DupSort, Encode, Table, TableInfo}, }; @@ -32,7 +32,9 @@ use reth_ethereum_primitives::{Receipt, TransactionSigned}; use reth_primitives_traits::{Account, Bytecode, StorageEntry}; use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::StageCheckpoint; -use reth_trie_common::{BranchNodeCompact, StorageTrieEntry, StoredNibbles, StoredNibblesSubKey}; +use reth_trie_common::{ + BranchNodeCompact, StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, TrieChangeSetsEntry, +}; use serde::{Deserialize, Serialize}; use std::fmt; @@ -486,6 +488,20 @@ tables! { type SubKey = StoredNibblesSubKey; } + /// Stores the state of a node in the accounts trie prior to a particular block being executed. + table AccountsTrieChangeSets { + type Key = BlockNumber; + type Value = TrieChangeSetsEntry; + type SubKey = StoredNibblesSubKey; + } + + /// Stores the state of a node in a storage trie prior to a particular block being executed. + table StoragesTrieChangeSets { + type Key = BlockNumberHashedAddress; + type Value = TrieChangeSetsEntry; + type SubKey = StoredNibblesSubKey; + } + /// Stores the transaction sender for each canonical transaction. /// It is needed to speed up execution stage and allows fetching signer without doing /// transaction signed recovery diff --git a/crates/storage/db-common/src/init.rs b/crates/storage/db-common/src/init.rs index 48442aab381..87f009356a0 100644 --- a/crates/storage/db-common/src/init.rs +++ b/crates/storage/db-common/src/init.rs @@ -602,7 +602,7 @@ where match state_root.root_with_progress()? { StateRootProgress::Progress(state, _, updates) => { - let updated_len = provider.write_trie_updates(&updates)?; + let updated_len = provider.write_trie_updates(updates)?; total_flushed_updates += updated_len; trace!(target: "reth::cli", @@ -622,7 +622,7 @@ where } } StateRootProgress::Complete(root, _, updates) => { - let updated_len = provider.write_trie_updates(&updates)?; + let updated_len = provider.write_trie_updates(updates)?; total_flushed_updates += updated_len; trace!(target: "reth::cli", diff --git a/crates/storage/errors/src/provider.rs b/crates/storage/errors/src/provider.rs index c27587690ba..47cc630bcb6 100644 --- a/crates/storage/errors/src/provider.rs +++ b/crates/storage/errors/src/provider.rs @@ -137,6 +137,14 @@ pub enum ProviderError { /// Missing trie updates. #[error("missing trie updates for block {0}")] MissingTrieUpdates(B256), + /// Insufficient changesets to revert to the requested block. + #[error("insufficient changesets to revert to block #{requested}. Available changeset range: {available:?}")] + InsufficientChangesets { + /// The block number requested for reversion + requested: BlockNumber, + /// The available range of blocks with changesets + available: core::ops::RangeInclusive, + }, /// Any other error type wrapped into a cloneable [`AnyError`]. #[error(transparent)] Other(#[from] AnyError), diff --git a/crates/storage/provider/src/bundle_state/mod.rs b/crates/storage/provider/src/bundle_state/mod.rs deleted file mode 100644 index 58b76f1eacf..00000000000 --- a/crates/storage/provider/src/bundle_state/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! Bundle state module. -//! This module contains all the logic related to bundle state. - -mod state_reverts; -pub use state_reverts::StorageRevertsIter; diff --git a/crates/storage/provider/src/changesets_utils/mod.rs b/crates/storage/provider/src/changesets_utils/mod.rs new file mode 100644 index 00000000000..3b65825264b --- /dev/null +++ b/crates/storage/provider/src/changesets_utils/mod.rs @@ -0,0 +1,7 @@ +//! This module contains helpful utilities related to populating changesets tables. + +mod state_reverts; +pub use state_reverts::StorageRevertsIter; + +mod trie; +pub use trie::*; diff --git a/crates/storage/provider/src/bundle_state/state_reverts.rs b/crates/storage/provider/src/changesets_utils/state_reverts.rs similarity index 100% rename from crates/storage/provider/src/bundle_state/state_reverts.rs rename to crates/storage/provider/src/changesets_utils/state_reverts.rs diff --git a/crates/storage/provider/src/changesets_utils/trie.rs b/crates/storage/provider/src/changesets_utils/trie.rs new file mode 100644 index 00000000000..f4365aab103 --- /dev/null +++ b/crates/storage/provider/src/changesets_utils/trie.rs @@ -0,0 +1,147 @@ +use itertools::{merge_join_by, EitherOrBoth}; +use reth_db_api::DatabaseError; +use reth_trie::{trie_cursor::TrieCursor, BranchNodeCompact, Nibbles}; +use std::cmp::{Ord, Ordering}; + +/// Combines a sorted iterator of trie node paths and a storage trie cursor into a new +/// iterator which produces the current values of all given paths in the same order. +#[derive(Debug)] +pub struct StorageTrieCurrentValuesIter<'cursor, P, C> { + /// Sorted iterator of node paths which we want the values of. + paths: P, + /// Storage trie cursor. + cursor: &'cursor mut C, + /// Current value at the cursor, allows us to treat the cursor as a peekable iterator. + cursor_current: Option<(Nibbles, BranchNodeCompact)>, +} + +impl<'cursor, P, C> StorageTrieCurrentValuesIter<'cursor, P, C> +where + P: Iterator, + C: TrieCursor, +{ + /// Instantiate a [`StorageTrieCurrentValuesIter`] from a sorted paths iterator and a cursor. + pub fn new(paths: P, cursor: &'cursor mut C) -> Result { + let mut new_self = Self { paths, cursor, cursor_current: None }; + new_self.seek_cursor(Nibbles::default())?; + Ok(new_self) + } + + fn seek_cursor(&mut self, path: Nibbles) -> Result<(), DatabaseError> { + self.cursor_current = self.cursor.seek(path)?; + Ok(()) + } +} + +impl<'cursor, P, C> Iterator for StorageTrieCurrentValuesIter<'cursor, P, C> +where + P: Iterator, + C: TrieCursor, +{ + type Item = Result<(Nibbles, Option), DatabaseError>; + + fn next(&mut self) -> Option { + let Some(curr_path) = self.paths.next() else { + // If there are no more paths then there is no further possible output. + return None + }; + + // If the path is ahead of the cursor then seek the cursor forward to catch up. The cursor + // will seek either to `curr_path` or beyond it. + if self.cursor_current.as_ref().is_some_and(|(cursor_path, _)| curr_path > *cursor_path) && + let Err(err) = self.seek_cursor(curr_path) + { + return Some(Err(err)) + } + + // If there is a path but the cursor is empty then that path has no node. + if self.cursor_current.is_none() { + return Some(Ok((curr_path, None))) + } + + let (cursor_path, cursor_node) = + self.cursor_current.as_mut().expect("already checked for None"); + + // There is both a path and a cursor value, compare their paths. + match curr_path.cmp(cursor_path) { + Ordering::Less => { + // If the path is behind the cursor then there is no value for that + // path, produce None. + Some(Ok((curr_path, None))) + } + Ordering::Equal => { + // If the target path and cursor's path match then there is a value for that path, + // return the value. We don't seek the cursor here, that will be handled on the + // next call to `next` after checking that `paths` isn't None. + let cursor_node = core::mem::take(cursor_node); + Some(Ok((*cursor_path, Some(cursor_node)))) + } + Ordering::Greater => { + panic!("cursor was seeked to {curr_path:?}, but produced a node at a lower path {cursor_path:?}") + } + } + } +} + +/// Returns an iterator which produces the values to be inserted into the `StoragesTrieChangeSets` +/// table for an account whose storage was wiped during a block. It is expected that this is called +/// prior to inserting the block's trie updates. +/// +/// ## Arguments +/// +/// - `curr_values_of_changed` is an iterator over the current values of all trie nodes modified by +/// the block, ordered by path. +/// - `all_nodes` is an iterator over all existing trie nodes for the account, ordered by path. +/// +/// ## Returns +/// +/// An iterator of trie node paths and a `Some(node)` (indicating the node was wiped) or a `None` +/// (indicating the node was modified in the block but didn't previously exist. The iterator's +/// results will be ordered by path. +pub fn storage_trie_wiped_changeset_iter( + curr_values_of_changed: impl Iterator< + Item = Result<(Nibbles, Option), DatabaseError>, + >, + all_nodes: impl Iterator>, +) -> Result< + impl Iterator), DatabaseError>>, + DatabaseError, +> { + let all_nodes = all_nodes.map(|e| e.map(|(nibbles, node)| (nibbles, Some(node)))); + + let merged = merge_join_by(curr_values_of_changed, all_nodes, |a, b| match (a, b) { + (Err(_), _) => Ordering::Less, + (_, Err(_)) => Ordering::Greater, + (Ok(a), Ok(b)) => a.0.cmp(&b.0), + }); + + Ok(merged.map(|either_or| match either_or { + EitherOrBoth::Left(changed) => { + // A path of a changed node (given in `paths`) which was not found in the database (or + // there's an error). The current value of this path must be None, otherwise it would + // have also been returned by the `all_nodes` iter. + debug_assert!( + changed.as_ref().is_err() || changed.as_ref().is_ok_and(|(_, node)| node.is_none()), + "changed node is Some but wasn't returned by `all_nodes` iterator: {changed:?}", + ); + changed + } + EitherOrBoth::Right(wiped) => { + // A node was found in the db (indicating it was wiped) but was not given in `paths`. + // Return it as-is. + wiped + } + EitherOrBoth::Both(changed, _wiped) => { + // A path of a changed node (given in `paths`) was found with a previous value in the + // database. The changed node must have a value which is equal to the one found by the + // `all_nodes` iterator. If the changed node had no previous value (None) it wouldn't + // be returned by `all_nodes` and so would be in the Left branch. + // + // Due to the ordering closure passed to `merge_join_by` it's not possible for either + // value to be an error here. + debug_assert!(changed.is_ok(), "unreachable error condition: {changed:?}"); + debug_assert_eq!(changed, _wiped); + changed + } + })) +} diff --git a/crates/storage/provider/src/lib.rs b/crates/storage/provider/src/lib.rs index c281f117908..70822c604bb 100644 --- a/crates/storage/provider/src/lib.rs +++ b/crates/storage/provider/src/lib.rs @@ -35,7 +35,7 @@ pub use static_file::StaticFileSegment; pub use reth_execution_types::*; -pub mod bundle_state; +pub mod changesets_utils; /// Re-export `OriginalValuesKnown` pub use revm_database::states::OriginalValuesKnown; diff --git a/crates/storage/provider/src/providers/blockchain_provider.rs b/crates/storage/provider/src/providers/blockchain_provider.rs index 7040032eca0..512b8569de2 100644 --- a/crates/storage/provider/src/providers/blockchain_provider.rs +++ b/crates/storage/provider/src/providers/blockchain_provider.rs @@ -6,7 +6,7 @@ use crate::{ HashedPostStateProvider, HeaderProvider, ProviderError, ProviderFactory, PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt, StageCheckpointReader, StateProviderBox, StateProviderFactory, StateReader, StaticFileProviderFactory, TransactionVariant, - TransactionsProvider, + TransactionsProvider, TrieReader, }; use alloy_consensus::transaction::TransactionMeta; use alloy_eips::{BlockHashOrNumber, BlockId, BlockNumHash, BlockNumberOrTag}; @@ -25,7 +25,7 @@ use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_storage_api::{BlockBodyIndicesProvider, NodePrimitivesProvider, StorageChangeSetReader}; use reth_storage_errors::provider::ProviderResult; -use reth_trie::{HashedPostState, KeccakKeyHasher}; +use reth_trie::{updates::TrieUpdatesSorted, HashedPostState, KeccakKeyHasher}; use revm_database::BundleState; use std::{ ops::{RangeBounds, RangeInclusive}, @@ -739,6 +739,19 @@ impl StateReader for BlockchainProvider { } } +impl TrieReader for BlockchainProvider { + fn trie_reverts(&self, from: BlockNumber) -> ProviderResult { + self.consistent_provider()?.trie_reverts(from) + } + + fn get_block_trie_updates( + &self, + block_number: BlockNumber, + ) -> ProviderResult { + self.consistent_provider()?.get_block_trie_updates(block_number) + } +} + #[cfg(test)] mod tests { use crate::{ @@ -755,8 +768,7 @@ mod tests { use rand::Rng; use reth_chain_state::{ test_utils::TestBlockBuilder, CanonStateNotification, CanonStateSubscriptions, - CanonicalInMemoryState, ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates, - NewCanonicalChain, + CanonicalInMemoryState, ExecutedBlock, NewCanonicalChain, }; use reth_chainspec::{ChainSpec, MAINNET}; use reth_db_api::models::{AccountBeforeTx, StoredBlockBodyIndices}; @@ -882,12 +894,14 @@ mod tests { let execution_outcome = ExecutionOutcome { receipts: vec![block_receipts], ..Default::default() }; - ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed(block.clone(), senders)), - execution_outcome.into(), - Default::default(), - ExecutedTrieUpdates::empty(), - ) + ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + block.clone(), + senders, + )), + execution_output: execution_outcome.into(), + ..Default::default() + } }) .collect(), }; @@ -1009,15 +1023,13 @@ mod tests { let in_memory_block_senders = first_in_mem_block.senders().expect("failed to recover senders"); let chain = NewCanonicalChain::Commit { - new: vec![ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed( + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( first_in_mem_block.clone(), in_memory_block_senders, )), - Default::default(), - Default::default(), - ExecutedTrieUpdates::empty(), - )], + ..Default::default() + }], }; provider.canonical_in_memory_state.update_chain(chain); @@ -1045,16 +1057,12 @@ mod tests { assert_eq!(provider.find_block_by_hash(first_db_block.hash(), BlockSource::Pending)?, None); // Insert the last block into the pending state - provider.canonical_in_memory_state.set_pending_block(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(RecoveredBlock::new_sealed( - last_in_mem_block.clone(), - Default::default(), - )), - execution_output: Default::default(), - hashed_state: Default::default(), - }, - trie: ExecutedTrieUpdates::empty(), + provider.canonical_in_memory_state.set_pending_block(ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + last_in_mem_block.clone(), + Default::default(), + )), + ..Default::default() }); // Now the last block should be found in memory @@ -1105,15 +1113,13 @@ mod tests { let in_memory_block_senders = first_in_mem_block.senders().expect("failed to recover senders"); let chain = NewCanonicalChain::Commit { - new: vec![ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed( + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( first_in_mem_block.clone(), in_memory_block_senders, )), - Default::default(), - Default::default(), - ExecutedTrieUpdates::empty(), - )], + ..Default::default() + }], }; provider.canonical_in_memory_state.update_chain(chain); @@ -1159,16 +1165,12 @@ mod tests { ); // Set the block as pending - provider.canonical_in_memory_state.set_pending_block(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(RecoveredBlock::new_sealed( - block.clone(), - block.senders().unwrap(), - )), - execution_output: Default::default(), - hashed_state: Default::default(), - }, - trie: ExecutedTrieUpdates::empty(), + provider.canonical_in_memory_state.set_pending_block(ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + block.clone(), + block.senders().unwrap(), + )), + ..Default::default() }); // Assertions related to the pending block @@ -1206,15 +1208,13 @@ mod tests { let in_memory_block_senders = first_in_mem_block.senders().expect("failed to recover senders"); let chain = NewCanonicalChain::Commit { - new: vec![ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed( + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( first_in_mem_block.clone(), in_memory_block_senders, )), - Default::default(), - Default::default(), - ExecutedTrieUpdates::empty(), - )], + ..Default::default() + }], }; provider.canonical_in_memory_state.update_chain(chain); @@ -1686,9 +1686,12 @@ mod tests { .first() .map(|block| { let senders = block.senders().expect("failed to recover senders"); - ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed(block.clone(), senders)), - Arc::new(ExecutionOutcome { + ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + block.clone(), + senders, + )), + execution_output: Arc::new(ExecutionOutcome { bundle: BundleState::new( in_memory_state.into_iter().map(|(address, (account, _))| { (address, None, Some(account.into()), Default::default()) @@ -1701,9 +1704,8 @@ mod tests { first_block: first_in_memory_block, ..Default::default() }), - Default::default(), - ExecutedTrieUpdates::empty(), - ) + ..Default::default() + } }) .unwrap()], }; @@ -1821,19 +1823,13 @@ mod tests { // adding a pending block to state can test pending() and pending_state_by_hash() function let pending_block = database_blocks[database_blocks.len() - 1].clone(); - only_database_provider.canonical_in_memory_state.set_pending_block( - ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(RecoveredBlock::new_sealed( - pending_block.clone(), - Default::default(), - )), - execution_output: Default::default(), - hashed_state: Default::default(), - }, - trie: ExecutedTrieUpdates::empty(), - }, - ); + only_database_provider.canonical_in_memory_state.set_pending_block(ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + pending_block.clone(), + Default::default(), + )), + ..Default::default() + }); assert_eq!( pending_block.hash(), @@ -1919,16 +1915,12 @@ mod tests { // Set the pending block in memory let pending_block = in_memory_blocks.last().unwrap(); - provider.canonical_in_memory_state.set_pending_block(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(RecoveredBlock::new_sealed( - pending_block.clone(), - Default::default(), - )), - execution_output: Default::default(), - hashed_state: Default::default(), - }, - trie: ExecutedTrieUpdates::empty(), + provider.canonical_in_memory_state.set_pending_block(ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + pending_block.clone(), + Default::default(), + )), + ..Default::default() }); // Set the safe block in memory diff --git a/crates/storage/provider/src/providers/consistent.rs b/crates/storage/provider/src/providers/consistent.rs index 93415e8e347..66a35e5e9b1 100644 --- a/crates/storage/provider/src/providers/consistent.rs +++ b/crates/storage/provider/src/providers/consistent.rs @@ -4,7 +4,7 @@ use crate::{ BlockReader, BlockReaderIdExt, BlockSource, ChainSpecProvider, ChangeSetReader, HeaderProvider, ProviderError, PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt, StageCheckpointReader, StateReader, StaticFileProviderFactory, TransactionVariant, - TransactionsProvider, + TransactionsProvider, TrieReader, }; use alloy_consensus::{transaction::TransactionMeta, BlockHeader}; use alloy_eips::{ @@ -28,6 +28,7 @@ use reth_storage_api::{ StorageChangeSetReader, TryIntoHistoricalStateProvider, }; use reth_storage_errors::provider::ProviderResult; +use reth_trie::updates::TrieUpdatesSorted; use revm_database::states::PlainStorageRevert; use std::{ ops::{Add, Bound, RangeBounds, RangeInclusive, Sub}, @@ -1504,6 +1505,19 @@ impl StateReader for ConsistentProvider { } } +impl TrieReader for ConsistentProvider { + fn trie_reverts(&self, from: BlockNumber) -> ProviderResult { + self.storage_provider.trie_reverts(from) + } + + fn get_block_trie_updates( + &self, + block_number: BlockNumber, + ) -> ProviderResult { + self.storage_provider.get_block_trie_updates(block_number) + } +} + #[cfg(test)] mod tests { use crate::{ @@ -1514,9 +1528,7 @@ mod tests { use alloy_primitives::B256; use itertools::Itertools; use rand::Rng; - use reth_chain_state::{ - ExecutedBlock, ExecutedBlockWithTrieUpdates, ExecutedTrieUpdates, NewCanonicalChain, - }; + use reth_chain_state::{ExecutedBlock, NewCanonicalChain}; use reth_db_api::models::AccountBeforeTx; use reth_ethereum_primitives::Block; use reth_execution_types::ExecutionOutcome; @@ -1619,15 +1631,13 @@ mod tests { let in_memory_block_senders = first_in_mem_block.senders().expect("failed to recover senders"); let chain = NewCanonicalChain::Commit { - new: vec![ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed( + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( first_in_mem_block.clone(), in_memory_block_senders, )), - Default::default(), - Default::default(), - ExecutedTrieUpdates::empty(), - )], + ..Default::default() + }], }; consistent_provider.canonical_in_memory_state.update_chain(chain); let consistent_provider = provider.consistent_provider()?; @@ -1661,16 +1671,12 @@ mod tests { ); // Insert the last block into the pending state - provider.canonical_in_memory_state.set_pending_block(ExecutedBlockWithTrieUpdates { - block: ExecutedBlock { - recovered_block: Arc::new(RecoveredBlock::new_sealed( - last_in_mem_block.clone(), - Default::default(), - )), - execution_output: Default::default(), - hashed_state: Default::default(), - }, - trie: ExecutedTrieUpdates::empty(), + provider.canonical_in_memory_state.set_pending_block(ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + last_in_mem_block.clone(), + Default::default(), + )), + ..Default::default() }); // Now the last block should be found in memory @@ -1729,15 +1735,13 @@ mod tests { let in_memory_block_senders = first_in_mem_block.senders().expect("failed to recover senders"); let chain = NewCanonicalChain::Commit { - new: vec![ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed( + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( first_in_mem_block.clone(), in_memory_block_senders, )), - Default::default(), - Default::default(), - ExecutedTrieUpdates::empty(), - )], + ..Default::default() + }], }; consistent_provider.canonical_in_memory_state.update_chain(chain); @@ -1834,9 +1838,12 @@ mod tests { .first() .map(|block| { let senders = block.senders().expect("failed to recover senders"); - ExecutedBlockWithTrieUpdates::new( - Arc::new(RecoveredBlock::new_sealed(block.clone(), senders)), - Arc::new(ExecutionOutcome { + ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + block.clone(), + senders, + )), + execution_output: Arc::new(ExecutionOutcome { bundle: BundleState::new( in_memory_state.into_iter().map(|(address, (account, _))| { (address, None, Some(account.into()), Default::default()) @@ -1849,9 +1856,8 @@ mod tests { first_block: first_in_memory_block, ..Default::default() }), - Default::default(), - ExecutedTrieUpdates::empty(), - ) + ..Default::default() + } }) .unwrap()], }; diff --git a/crates/storage/provider/src/providers/database/mod.rs b/crates/storage/provider/src/providers/database/mod.rs index f7b3c4ba603..bd6b1e0f472 100644 --- a/crates/storage/provider/src/providers/database/mod.rs +++ b/crates/storage/provider/src/providers/database/mod.rs @@ -84,7 +84,7 @@ impl ProviderFactory { db, chain_spec, static_file_provider, - prune_modes: PruneModes::none(), + prune_modes: PruneModes::default(), storage: Default::default(), } } @@ -131,7 +131,7 @@ impl>> ProviderFactory { db: Arc::new(init_db(path, args).map_err(RethError::msg)?), chain_spec, static_file_provider, - prune_modes: PruneModes::none(), + prune_modes: PruneModes::default(), storage: Default::default(), }) } @@ -670,7 +670,7 @@ mod tests { let prune_modes = PruneModes { sender_recovery: Some(PruneMode::Full), transaction_lookup: Some(PruneMode::Full), - ..PruneModes::none() + ..PruneModes::default() }; let factory = create_test_provider_factory(); let provider = factory.with_prune_modes(prune_modes).provider_rw().unwrap(); diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index 6fdc37c4f53..235bf57a4a4 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -1,5 +1,7 @@ use crate::{ - bundle_state::StorageRevertsIter, + changesets_utils::{ + storage_trie_wiped_changeset_iter, StorageRevertsIter, StorageTrieCurrentValuesIter, + }, providers::{ database::{chain::ChainStorage, metrics}, static_file::StaticFileWriter, @@ -16,7 +18,7 @@ use crate::{ OriginalValuesKnown, ProviderError, PruneCheckpointReader, PruneCheckpointWriter, RevertsInit, StageCheckpointReader, StateProviderBox, StateWriter, StaticFileProviderFactory, StatsReader, StorageReader, StorageTrieWriter, TransactionVariant, TransactionsProvider, - TransactionsProviderExt, TrieWriter, + TransactionsProviderExt, TrieReader, TrieWriter, }; use alloy_consensus::{ transaction::{SignerRecoverable, TransactionMeta, TxHashRef}, @@ -30,14 +32,14 @@ use alloy_primitives::{ }; use itertools::Itertools; use rayon::slice::ParallelSliceMut; -use reth_chain_state::{ExecutedBlock, ExecutedBlockWithTrieUpdates}; +use reth_chain_state::ExecutedBlock; use reth_chainspec::{ChainInfo, ChainSpecProvider, EthChainSpec, EthereumHardforks}; use reth_db_api::{ cursor::{DbCursorRO, DbCursorRW, DbDupCursorRO, DbDupCursorRW}, database::Database, models::{ sharded_key, storage_sharded_key::StorageShardedKey, AccountBeforeTx, BlockNumberAddress, - ShardedKey, StoredBlockBodyIndices, + BlockNumberHashedAddress, ShardedKey, StoredBlockBodyIndices, }, table::Table, tables, @@ -47,8 +49,7 @@ use reth_db_api::{ use reth_execution_types::{Chain, ExecutionOutcome}; use reth_node_types::{BlockTy, BodyTy, HeaderTy, NodeTypes, ReceiptTy, TxTy}; use reth_primitives_traits::{ - Account, Block as _, BlockBody as _, Bytecode, GotExpected, RecoveredBlock, SealedHeader, - StorageEntry, + Account, Block as _, BlockBody as _, Bytecode, RecoveredBlock, SealedHeader, StorageEntry, }; use reth_prune_types::{ PruneCheckpoint, PruneMode, PruneModes, PruneSegment, MINIMUM_PRUNING_DISTANCE, @@ -59,13 +60,19 @@ use reth_storage_api::{ BlockBodyIndicesProvider, BlockBodyReader, NodePrimitivesProvider, StateProvider, StorageChangeSetReader, TryIntoHistoricalStateProvider, }; -use reth_storage_errors::provider::{ProviderResult, RootMismatch}; +use reth_storage_errors::provider::ProviderResult; use reth_trie::{ - prefix_set::{PrefixSet, PrefixSetMut, TriePrefixSets}, - updates::{StorageTrieUpdates, TrieUpdates}, - HashedPostStateSorted, Nibbles, StateRoot, StoredNibbles, + trie_cursor::{ + InMemoryTrieCursor, InMemoryTrieCursorFactory, TrieCursor, TrieCursorFactory, + TrieCursorIter, + }, + updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted}, + BranchNodeCompact, HashedPostStateSorted, Nibbles, StoredNibbles, StoredNibblesSubKey, + TrieChangeSetsEntry, +}; +use reth_trie_db::{ + DatabaseAccountTrieCursor, DatabaseStorageTrieCursor, DatabaseTrieCursorFactory, }; -use reth_trie_db::{DatabaseStateRoot, DatabaseStorageTrieCursor}; use revm_database::states::{ PlainStateReverts, PlainStorageChangeset, PlainStorageRevert, StateChangeset, }; @@ -73,7 +80,7 @@ use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet}, fmt::Debug, - ops::{Deref, DerefMut, Not, Range, RangeBounds, RangeInclusive}, + ops::{Deref, DerefMut, Not, Range, RangeBounds, RangeFrom, RangeInclusive}, sync::Arc, }; use tracing::{debug, trace}; @@ -254,10 +261,7 @@ impl AsRef for DatabaseProvider { impl DatabaseProvider { /// Writes executed blocks and state to storage. - pub fn save_blocks( - &self, - blocks: Vec>, - ) -> ProviderResult<()> { + pub fn save_blocks(&self, blocks: Vec>) -> ProviderResult<()> { if blocks.is_empty() { debug!(target: "providers::db", "Attempted to write empty block range"); return Ok(()) @@ -281,12 +285,10 @@ impl DatabaseProvider DatabaseProvider DatabaseProvider, - ) -> ProviderResult<()> { + pub fn unwind_trie_state_from(&self, from: BlockNumber) -> ProviderResult<()> { let changed_accounts = self .tx .cursor_read::()? - .walk_range(range.clone())? + .walk_range(from..)? .collect::, _>>()?; - // Unwind account hashes. Add changed accounts to account prefix set. - let hashed_addresses = self.unwind_account_hashing(changed_accounts.iter())?; - let mut account_prefix_set = PrefixSetMut::with_capacity(hashed_addresses.len()); - let mut destroyed_accounts = HashSet::default(); - for (hashed_address, account) in hashed_addresses { - account_prefix_set.insert(Nibbles::unpack(hashed_address)); - if account.is_none() { - destroyed_accounts.insert(hashed_address); - } - } + // Unwind account hashes. + self.unwind_account_hashing(changed_accounts.iter())?; // Unwind account history indices. self.unwind_account_history_indices(changed_accounts.iter())?; - let storage_range = BlockNumberAddress::range(range.clone()); + let storage_start = BlockNumberAddress((from, Address::ZERO)); let changed_storages = self .tx .cursor_read::()? - .walk_range(storage_range)? + .walk_range(storage_start..)? .collect::, _>>()?; - // Unwind storage hashes. Add changed account and storage keys to corresponding prefix - // sets. - let mut storage_prefix_sets = B256Map::::default(); - let storage_entries = self.unwind_storage_hashing(changed_storages.iter().copied())?; - for (hashed_address, hashed_slots) in storage_entries { - account_prefix_set.insert(Nibbles::unpack(hashed_address)); - let mut storage_prefix_set = PrefixSetMut::with_capacity(hashed_slots.len()); - for slot in hashed_slots { - storage_prefix_set.insert(Nibbles::unpack(slot)); - } - storage_prefix_sets.insert(hashed_address, storage_prefix_set.freeze()); - } + // Unwind storage hashes. + self.unwind_storage_hashing(changed_storages.iter().copied())?; // Unwind storage history indices. self.unwind_storage_history_indices(changed_storages.iter().copied())?; - // Calculate the reverted merkle root. - // This is the same as `StateRoot::incremental_root_with_updates`, only the prefix sets - // are pre-loaded. - let prefix_sets = TriePrefixSets { - account_prefix_set: account_prefix_set.freeze(), - storage_prefix_sets, - destroyed_accounts, - }; - let (new_state_root, trie_updates) = StateRoot::from_tx(&self.tx) - .with_prefix_sets(prefix_sets) - .root_with_updates() - .map_err(reth_db_api::DatabaseError::from)?; - - let parent_number = range.start().saturating_sub(1); - let parent_state_root = self - .header_by_number(parent_number)? - .ok_or_else(|| ProviderError::HeaderNotFound(parent_number.into()))? - .state_root(); - - // state root should be always correct as we are reverting state. - // but for sake of double verification we will check it again. - if new_state_root != parent_state_root { - let parent_hash = self - .block_hash(parent_number)? - .ok_or_else(|| ProviderError::HeaderNotFound(parent_number.into()))?; - return Err(ProviderError::UnwindStateRootMismatch(Box::new(RootMismatch { - root: GotExpected { got: new_state_root, expected: parent_state_root }, - block_number: parent_number, - block_hash: parent_hash, - }))) - } - self.write_trie_updates(&trie_updates)?; + // Unwind accounts/storages trie tables using the revert. + let trie_revert = self.trie_reverts(from)?; + self.write_trie_updates_sorted(&trie_revert)?; + + // Clear trie changesets which have been unwound. + self.clear_trie_changesets_from(from)?; Ok(()) } @@ -1773,6 +1730,10 @@ impl StateWriter // If we are writing the primary storage wipe transition, the pre-existing plain // storage state has to be taken from the database and written to storage history. // See [StorageWipe::Primary] for more details. + // + // TODO(mediocregopher): This could be rewritten in a way which doesn't require + // collecting wiped entries into a Vec like this, see + // `write_storage_trie_changesets`. let mut wiped_storage = Vec::new(); if wiped { tracing::trace!(?address, "Wiping storage"); @@ -2143,8 +2104,10 @@ impl StateWriter } impl TrieWriter for DatabaseProvider { - /// Writes trie updates. Returns the number of entries modified. - fn write_trie_updates(&self, trie_updates: &TrieUpdates) -> ProviderResult { + /// Writes trie updates to the database with already sorted updates. + /// + /// Returns the number of entries modified. + fn write_trie_updates_sorted(&self, trie_updates: &TrieUpdatesSorted) -> ProviderResult { if trie_updates.is_empty() { return Ok(0) } @@ -2152,23 +2115,11 @@ impl TrieWriter for DatabaseProvider // Track the number of inserted entries. let mut num_entries = 0; - // Merge updated and removed nodes. Updated nodes must take precedence. - let mut account_updates = trie_updates - .removed_nodes_ref() - .iter() - .filter_map(|n| { - (!trie_updates.account_nodes_ref().contains_key(n)).then_some((n, None)) - }) - .collect::>(); - account_updates.extend( - trie_updates.account_nodes_ref().iter().map(|(nibbles, node)| (nibbles, Some(node))), - ); - // Sort trie node updates. - account_updates.sort_unstable_by(|a, b| a.0.cmp(b.0)); - let tx = self.tx_ref(); let mut account_trie_cursor = tx.cursor_write::()?; - for (key, updated_node) in account_updates { + + // Process sorted account nodes + for (key, updated_node) in &trie_updates.account_nodes { let nibbles = StoredNibbles(*key); match updated_node { Some(node) => { @@ -2186,18 +2137,226 @@ impl TrieWriter for DatabaseProvider } } - num_entries += self.write_storage_trie_updates(trie_updates.storage_tries_ref().iter())?; + num_entries += + self.write_storage_trie_updates_sorted(trie_updates.storage_tries_ref().iter())?; + + Ok(num_entries) + } + + /// Records the current values of all trie nodes which will be updated using the `TrieUpdates` + /// into the trie changesets tables. + /// + /// The intended usage of this method is to call it _prior_ to calling `write_trie_updates` with + /// the same `TrieUpdates`. + /// + /// Returns the number of keys written. + fn write_trie_changesets( + &self, + block_number: BlockNumber, + trie_updates: &TrieUpdatesSorted, + updates_overlay: Option<&TrieUpdatesSorted>, + ) -> ProviderResult { + let mut num_entries = 0; + + let mut changeset_cursor = + self.tx_ref().cursor_dup_write::()?; + let curr_values_cursor = self.tx_ref().cursor_read::()?; + + // Wrap the cursor in DatabaseAccountTrieCursor + let mut db_account_cursor = DatabaseAccountTrieCursor::new(curr_values_cursor); + + // Static empty array for when updates_overlay is None + static EMPTY_ACCOUNT_UPDATES: Vec<(Nibbles, Option)> = Vec::new(); + + // Get the overlay updates for account trie, or use an empty array + let account_overlay_updates = updates_overlay + .map(|overlay| overlay.account_nodes_ref()) + .unwrap_or(&EMPTY_ACCOUNT_UPDATES); + + // Wrap the cursor in InMemoryTrieCursor with the overlay + let mut in_memory_account_cursor = + InMemoryTrieCursor::new(Some(&mut db_account_cursor), account_overlay_updates); + + for (path, _) in trie_updates.account_nodes_ref() { + num_entries += 1; + let node = in_memory_account_cursor.seek_exact(*path)?.map(|(_, node)| node); + changeset_cursor.append_dup( + block_number, + TrieChangeSetsEntry { nibbles: StoredNibblesSubKey(*path), node }, + )?; + } + + let mut storage_updates = trie_updates.storage_tries.iter().collect::>(); + storage_updates.sort_unstable_by(|a, b| a.0.cmp(b.0)); + + num_entries += self.write_storage_trie_changesets( + block_number, + storage_updates.into_iter(), + updates_overlay, + )?; Ok(num_entries) } + + fn clear_trie_changesets(&self) -> ProviderResult<()> { + let tx = self.tx_ref(); + tx.clear::()?; + tx.clear::()?; + Ok(()) + } + + fn clear_trie_changesets_from(&self, from: BlockNumber) -> ProviderResult<()> { + let tx = self.tx_ref(); + { + let range = from..; + let mut cursor = tx.cursor_dup_write::()?; + let mut walker = cursor.walk_range(range)?; + + while walker.next().transpose()?.is_some() { + walker.delete_current()?; + } + } + + { + let range: RangeFrom = (from, B256::ZERO).into()..; + let mut cursor = tx.cursor_dup_write::()?; + let mut walker = cursor.walk_range(range)?; + + while walker.next().transpose()?.is_some() { + walker.delete_current()?; + } + } + + Ok(()) + } +} + +impl TrieReader for DatabaseProvider { + fn trie_reverts(&self, from: BlockNumber) -> ProviderResult { + let tx = self.tx_ref(); + + // Read account trie changes directly into a Vec - data is already sorted by nibbles + // within each block, and we want the oldest (first) version of each node + let mut account_nodes = Vec::new(); + let mut seen_account_keys = HashSet::new(); + let mut accounts_cursor = tx.cursor_dup_read::()?; + + for entry in accounts_cursor.walk_range(from..)? { + let (_, TrieChangeSetsEntry { nibbles, node }) = entry?; + // Only keep the first (oldest) version of each node + if seen_account_keys.insert(nibbles.0) { + account_nodes.push((nibbles.0, node)); + } + } + + // Read storage trie changes - data is sorted by (block, hashed_address, nibbles) + // Keep track of seen (address, nibbles) pairs to only keep the oldest version + let mut storage_tries = B256Map::>::default(); + let mut seen_storage_keys = HashSet::new(); + let mut storages_cursor = tx.cursor_dup_read::()?; + + // Create storage range starting from `from` block + let storage_range_start = BlockNumberHashedAddress((from, B256::ZERO)); + + for entry in storages_cursor.walk_range(storage_range_start..)? { + let ( + BlockNumberHashedAddress((_, hashed_address)), + TrieChangeSetsEntry { nibbles, node }, + ) = entry?; + + // Only keep the first (oldest) version of each node for this address + if seen_storage_keys.insert((hashed_address, nibbles.0)) { + storage_tries.entry(hashed_address).or_default().push((nibbles.0, node)); + } + } + + // Convert to StorageTrieUpdatesSorted + let storage_tries = storage_tries + .into_iter() + .map(|(address, nodes)| { + (address, StorageTrieUpdatesSorted { storage_nodes: nodes, is_deleted: false }) + }) + .collect(); + + Ok(TrieUpdatesSorted { account_nodes, storage_tries }) + } + + fn get_block_trie_updates( + &self, + block_number: BlockNumber, + ) -> ProviderResult { + let tx = self.tx_ref(); + + // Step 1: Get the trie reverts for the state after the target block + let reverts = self.trie_reverts(block_number + 1)?; + + // Step 2: Create an InMemoryTrieCursorFactory with the reverts + // This gives us the trie state as it was after the target block was processed + let db_cursor_factory = DatabaseTrieCursorFactory::new(tx); + let cursor_factory = InMemoryTrieCursorFactory::new(db_cursor_factory, &reverts); + + // Step 3: Collect all account trie nodes that changed in the target block + let mut trie_updates = TrieUpdatesSorted::default(); + + // Walk through all account trie changes for this block + let mut accounts_trie_cursor = tx.cursor_dup_read::()?; + let mut account_cursor = cursor_factory.account_trie_cursor()?; + + for entry in accounts_trie_cursor.walk_dup(Some(block_number), None)? { + let (_, TrieChangeSetsEntry { nibbles, .. }) = entry?; + // Look up the current value of this trie node using the overlay cursor + let node_value = account_cursor.seek_exact(nibbles.0)?.map(|(_, node)| node); + trie_updates.account_nodes.push((nibbles.0, node_value)); + } + + // Step 4: Collect all storage trie nodes that changed in the target block + let mut storages_trie_cursor = tx.cursor_dup_read::()?; + let storage_range_start = BlockNumberHashedAddress((block_number, B256::ZERO)); + let storage_range_end = BlockNumberHashedAddress((block_number + 1, B256::ZERO)); + + let mut current_hashed_address = None; + let mut storage_cursor = None; + + for entry in storages_trie_cursor.walk_range(storage_range_start..storage_range_end)? { + let ( + BlockNumberHashedAddress((_, hashed_address)), + TrieChangeSetsEntry { nibbles, .. }, + ) = entry?; + + // Check if we need to create a new storage cursor for a different account + if current_hashed_address != Some(hashed_address) { + storage_cursor = Some(cursor_factory.storage_trie_cursor(hashed_address)?); + current_hashed_address = Some(hashed_address); + } + + // Look up the current value of this storage trie node + let cursor = + storage_cursor.as_mut().expect("storage_cursor was just initialized above"); + let node_value = cursor.seek_exact(nibbles.0)?.map(|(_, node)| node); + trie_updates + .storage_tries + .entry(hashed_address) + .or_insert_with(|| StorageTrieUpdatesSorted { + storage_nodes: Vec::new(), + is_deleted: false, + }) + .storage_nodes + .push((nibbles.0, node_value)); + } + + Ok(trie_updates) + } } impl StorageTrieWriter for DatabaseProvider { - /// Writes storage trie updates from the given storage trie map. First sorts the storage trie - /// updates by the hashed address, writing in sorted order. - fn write_storage_trie_updates<'a>( + /// Writes storage trie updates from the given storage trie map with already sorted updates. + /// + /// Expects the storage trie updates to already be sorted by the hashed address key. + /// + /// Returns the number of entries modified. + fn write_storage_trie_updates_sorted<'a>( &self, - storage_tries: impl Iterator, + storage_tries: impl Iterator, ) -> ProviderResult { let mut num_entries = 0; let mut storage_tries = storage_tries.collect::>(); @@ -2207,12 +2366,110 @@ impl StorageTrieWriter for DatabaseP let mut db_storage_trie_cursor = DatabaseStorageTrieCursor::new(cursor, *hashed_address); num_entries += - db_storage_trie_cursor.write_storage_trie_updates(storage_trie_updates)?; + db_storage_trie_cursor.write_storage_trie_updates_sorted(storage_trie_updates)?; cursor = db_storage_trie_cursor.cursor; } Ok(num_entries) } + + /// Records the current values of all trie nodes which will be updated using the + /// `StorageTrieUpdates` into the storage trie changesets table. + /// + /// The intended usage of this method is to call it _prior_ to calling + /// `write_storage_trie_updates` with the same set of `StorageTrieUpdates`. + /// + /// Returns the number of keys written. + fn write_storage_trie_changesets<'a>( + &self, + block_number: BlockNumber, + storage_tries: impl Iterator, + updates_overlay: Option<&TrieUpdatesSorted>, + ) -> ProviderResult { + let mut num_written = 0; + + let mut changeset_cursor = + self.tx_ref().cursor_dup_write::()?; + + // We hold two cursors to the same table because we use them simultaneously when an + // account's storage is wiped. We keep them outside the for-loop so they can be re-used + // between accounts. + let changed_curr_values_cursor = self.tx_ref().cursor_dup_read::()?; + let wiped_nodes_cursor = self.tx_ref().cursor_dup_read::()?; + + // DatabaseStorageTrieCursor requires ownership of the cursor. The easiest way to deal with + // this is to create this outer variable with an initial dummy account, and overwrite it on + // every loop for every real account. + let mut changed_curr_values_cursor = DatabaseStorageTrieCursor::new( + changed_curr_values_cursor, + B256::default(), // Will be set per iteration + ); + let mut wiped_nodes_cursor = DatabaseStorageTrieCursor::new( + wiped_nodes_cursor, + B256::default(), // Will be set per iteration + ); + + // Static empty array for when updates_overlay is None + static EMPTY_UPDATES: Vec<(Nibbles, Option)> = Vec::new(); + + for (hashed_address, storage_trie_updates) in storage_tries { + let changeset_key = BlockNumberHashedAddress((block_number, *hashed_address)); + + // Update the hashed address for the cursors + changed_curr_values_cursor = + DatabaseStorageTrieCursor::new(changed_curr_values_cursor.cursor, *hashed_address); + + // Get the overlay updates for this storage trie, or use an empty array + let overlay_updates = updates_overlay + .and_then(|overlay| overlay.storage_tries.get(hashed_address)) + .map(|updates| updates.storage_nodes_ref()) + .unwrap_or(&EMPTY_UPDATES); + + // Wrap the cursor in InMemoryTrieCursor with the overlay + let mut in_memory_changed_cursor = + InMemoryTrieCursor::new(Some(&mut changed_curr_values_cursor), overlay_updates); + + // Create an iterator which produces the current values of all updated paths, or None if + // they are currently unset. + let curr_values_of_changed = StorageTrieCurrentValuesIter::new( + storage_trie_updates.storage_nodes.iter().map(|e| e.0), + &mut in_memory_changed_cursor, + )?; + + if storage_trie_updates.is_deleted() { + // Create an iterator that starts from the beginning of the storage trie for this + // account + wiped_nodes_cursor = + DatabaseStorageTrieCursor::new(wiped_nodes_cursor.cursor, *hashed_address); + + // Wrap the wiped nodes cursor in InMemoryTrieCursor with the overlay + let mut in_memory_wiped_cursor = + InMemoryTrieCursor::new(Some(&mut wiped_nodes_cursor), overlay_updates); + + let all_nodes = TrieCursorIter::new(&mut in_memory_wiped_cursor); + + for wiped in storage_trie_wiped_changeset_iter(curr_values_of_changed, all_nodes)? { + let (path, node) = wiped?; + num_written += 1; + changeset_cursor.append_dup( + changeset_key, + TrieChangeSetsEntry { nibbles: StoredNibblesSubKey(path), node }, + )?; + } + } else { + for curr_value in curr_values_of_changed { + let (path, node) = curr_value?; + num_written += 1; + changeset_cursor.append_dup( + changeset_key, + TrieChangeSetsEntry { nibbles: StoredNibblesSubKey(path), node }, + )?; + } + } + } + + Ok(num_written) + } } impl HashingWriter for DatabaseProvider { @@ -2507,7 +2764,7 @@ impl BlockExecu ) -> ProviderResult> { let range = block + 1..=self.last_block_number()?; - self.unwind_trie_state_range(range.clone())?; + self.unwind_trie_state_from(block + 1)?; // get execution res let execution_state = self.take_state_above(block)?; @@ -2525,9 +2782,7 @@ impl BlockExecu } fn remove_block_and_execution_above(&self, block: BlockNumber) -> ProviderResult<()> { - let range = block + 1..=self.last_block_number()?; - - self.unwind_trie_state_range(range)?; + self.unwind_trie_state_from(block + 1)?; // remove execution res self.remove_state_above(block)?; @@ -3139,4 +3394,1275 @@ mod tests { assert_eq!(range_result, individual_results); } + + #[test] + fn test_write_trie_changesets() { + use reth_db_api::models::BlockNumberHashedAddress; + use reth_trie::{BranchNodeCompact, StorageTrieEntry}; + + let factory = create_test_provider_factory(); + let provider_rw = factory.provider_rw().unwrap(); + + let block_number = 1u64; + + // Create some test nibbles and nodes + let account_nibbles1 = Nibbles::from_nibbles([0x1, 0x2, 0x3, 0x4]); + let account_nibbles2 = Nibbles::from_nibbles([0x5, 0x6, 0x7, 0x8]); + + let node1 = BranchNodeCompact::new( + 0b1111_1111_1111_1111, // state_mask + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask + vec![], // hashes + None, // root hash + ); + + // Pre-populate AccountsTrie with a node that will be updated (for account_nibbles1) + { + let mut cursor = provider_rw.tx_ref().cursor_write::().unwrap(); + cursor.insert(StoredNibbles(account_nibbles1), &node1).unwrap(); + } + + // Create account trie updates: one Some (update) and one None (removal) + let account_nodes = vec![ + (account_nibbles1, Some(node1.clone())), // This will update existing node + (account_nibbles2, None), // This will be a removal (no existing node) + ]; + + // Create storage trie updates + let storage_address1 = B256::from([1u8; 32]); // Normal storage trie + let storage_address2 = B256::from([2u8; 32]); // Wiped storage trie + + let storage_nibbles1 = Nibbles::from_nibbles([0xa, 0xb]); + let storage_nibbles2 = Nibbles::from_nibbles([0xc, 0xd]); + let storage_nibbles3 = Nibbles::from_nibbles([0xe, 0xf]); + + let storage_node1 = BranchNodeCompact::new( + 0b1111_0000_0000_0000, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + let storage_node2 = BranchNodeCompact::new( + 0b0000_1111_0000_0000, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Create an old version of storage_node1 to prepopulate + let storage_node1_old = BranchNodeCompact::new( + 0b1010_0000_0000_0000, // Different mask to show it's an old value + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Pre-populate StoragesTrie for normal storage (storage_address1) + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + // Add node that will be updated (storage_nibbles1) with old value + let entry = StorageTrieEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: storage_node1_old.clone(), + }; + cursor.upsert(storage_address1, &entry).unwrap(); + } + + // Pre-populate StoragesTrie for wiped storage (storage_address2) + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + // Add node that will be updated (storage_nibbles1) + let entry1 = StorageTrieEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: storage_node1.clone(), + }; + cursor.upsert(storage_address2, &entry1).unwrap(); + // Add node that won't be updated but exists (storage_nibbles3) + let entry3 = StorageTrieEntry { + nibbles: StoredNibblesSubKey(storage_nibbles3), + node: storage_node2.clone(), + }; + cursor.upsert(storage_address2, &entry3).unwrap(); + } + + // Normal storage trie: one Some (update) and one None (new) + let storage_trie1 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (storage_nibbles1, Some(storage_node1.clone())), // This will update existing node + (storage_nibbles2, None), // This is a new node + ], + }; + + // Wiped storage trie + let storage_trie2 = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![ + (storage_nibbles1, Some(storage_node1.clone())), // Updated node already in db + (storage_nibbles2, Some(storage_node2.clone())), /* Updated node not in db + * storage_nibbles3 is in db + * but not updated */ + ], + }; + + let mut storage_tries = B256Map::default(); + storage_tries.insert(storage_address1, storage_trie1); + storage_tries.insert(storage_address2, storage_trie2); + + let trie_updates = TrieUpdatesSorted { account_nodes, storage_tries }; + + // Write the changesets + let num_written = + provider_rw.write_trie_changesets(block_number, &trie_updates, None).unwrap(); + + // Verify number of entries written + // Account changesets: 2 (one update, one removal) + // Storage changesets: + // - Normal storage: 2 (one update, one removal) + // - Wiped storage: 3 (two updated, one existing not updated) + // Total: 2 + 2 + 3 = 7 + assert_eq!(num_written, 7); + + // Verify account changesets were written correctly + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_read::().unwrap(); + + // Get all entries for this block to see what was written + let all_entries = cursor + .walk_dup(Some(block_number), None) + .unwrap() + .collect::, _>>() + .unwrap(); + + // Assert the full value of all_entries in a single assert_eq + assert_eq!( + all_entries, + vec![ + ( + block_number, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles1), + node: Some(node1), + } + ), + ( + block_number, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles2), + node: None, + } + ), + ] + ); + } + + // Verify storage changesets were written correctly + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_read::().unwrap(); + + // Check normal storage trie changesets + let key1 = BlockNumberHashedAddress((block_number, storage_address1)); + let entries1 = + cursor.walk_dup(Some(key1), None).unwrap().collect::, _>>().unwrap(); + + assert_eq!( + entries1, + vec![ + ( + key1, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: Some(storage_node1_old), // Old value that was prepopulated + } + ), + ( + key1, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles2), + node: None, // New node, no previous value + } + ), + ] + ); + + // Check wiped storage trie changesets + let key2 = BlockNumberHashedAddress((block_number, storage_address2)); + let entries2 = + cursor.walk_dup(Some(key2), None).unwrap().collect::, _>>().unwrap(); + + assert_eq!( + entries2, + vec![ + ( + key2, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: Some(storage_node1), // Was in db, so has old value + } + ), + ( + key2, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles2), + node: None, // Was not in db + } + ), + ( + key2, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles3), + node: Some(storage_node2), // Existing node in wiped storage + } + ), + ] + ); + } + + provider_rw.commit().unwrap(); + } + + #[test] + fn test_write_trie_changesets_with_overlay() { + use reth_db_api::models::BlockNumberHashedAddress; + use reth_trie::BranchNodeCompact; + + let factory = create_test_provider_factory(); + let provider_rw = factory.provider_rw().unwrap(); + + let block_number = 1u64; + + // Create some test nibbles and nodes + let account_nibbles1 = Nibbles::from_nibbles([0x1, 0x2, 0x3, 0x4]); + let account_nibbles2 = Nibbles::from_nibbles([0x5, 0x6, 0x7, 0x8]); + + let node1 = BranchNodeCompact::new( + 0b1111_1111_1111_1111, // state_mask + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask + vec![], // hashes + None, // root hash + ); + + // NOTE: Unlike the previous test, we're NOT pre-populating the database + // All node values will come from the overlay + + // Create the overlay with existing values that would normally be in the DB + let node1_old = BranchNodeCompact::new( + 0b1010_1010_1010_1010, // Different mask to show it's the overlay "existing" value + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Create overlay account nodes + let overlay_account_nodes = vec![ + (account_nibbles1, Some(node1_old.clone())), // This simulates existing node in overlay + ]; + + // Create account trie updates: one Some (update) and one None (removal) + let account_nodes = vec![ + (account_nibbles1, Some(node1)), // This will update overlay node + (account_nibbles2, None), // This will be a removal (no existing node) + ]; + + // Create storage trie updates + let storage_address1 = B256::from([1u8; 32]); // Normal storage trie + let storage_address2 = B256::from([2u8; 32]); // Wiped storage trie + + let storage_nibbles1 = Nibbles::from_nibbles([0xa, 0xb]); + let storage_nibbles2 = Nibbles::from_nibbles([0xc, 0xd]); + let storage_nibbles3 = Nibbles::from_nibbles([0xe, 0xf]); + + let storage_node1 = BranchNodeCompact::new( + 0b1111_0000_0000_0000, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + let storage_node2 = BranchNodeCompact::new( + 0b0000_1111_0000_0000, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Create old versions for overlay + let storage_node1_old = BranchNodeCompact::new( + 0b1010_0000_0000_0000, // Different mask to show it's an old value + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Create overlay storage nodes + let mut overlay_storage_tries = B256Map::default(); + + // Overlay for normal storage (storage_address1) + let overlay_storage_trie1 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (storage_nibbles1, Some(storage_node1_old.clone())), /* Simulates existing in + * overlay */ + ], + }; + + // Overlay for wiped storage (storage_address2) + let overlay_storage_trie2 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (storage_nibbles1, Some(storage_node1.clone())), // Existing in overlay + (storage_nibbles3, Some(storage_node2.clone())), // Also existing in overlay + ], + }; + + overlay_storage_tries.insert(storage_address1, overlay_storage_trie1); + overlay_storage_tries.insert(storage_address2, overlay_storage_trie2); + + let overlay = TrieUpdatesSorted { + account_nodes: overlay_account_nodes, + storage_tries: overlay_storage_tries, + }; + + // Normal storage trie: one Some (update) and one None (new) + let storage_trie1 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (storage_nibbles1, Some(storage_node1.clone())), // This will update overlay node + (storage_nibbles2, None), // This is a new node + ], + }; + + // Wiped storage trie + let storage_trie2 = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![ + (storage_nibbles1, Some(storage_node1.clone())), // Updated node from overlay + (storage_nibbles2, Some(storage_node2.clone())), /* Updated node not in overlay + * storage_nibbles3 is in + * overlay + * but not updated */ + ], + }; + + let mut storage_tries = B256Map::default(); + storage_tries.insert(storage_address1, storage_trie1); + storage_tries.insert(storage_address2, storage_trie2); + + let trie_updates = TrieUpdatesSorted { account_nodes, storage_tries }; + + // Write the changesets WITH OVERLAY + let num_written = + provider_rw.write_trie_changesets(block_number, &trie_updates, Some(&overlay)).unwrap(); + + // Verify number of entries written + // Account changesets: 2 (one update from overlay, one removal) + // Storage changesets: + // - Normal storage: 2 (one update from overlay, one new) + // - Wiped storage: 3 (two updated, one existing from overlay not updated) + // Total: 2 + 2 + 3 = 7 + assert_eq!(num_written, 7); + + // Verify account changesets were written correctly + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_read::().unwrap(); + + // Get all entries for this block to see what was written + let all_entries = cursor + .walk_dup(Some(block_number), None) + .unwrap() + .collect::, _>>() + .unwrap(); + + // Assert the full value of all_entries in a single assert_eq + assert_eq!( + all_entries, + vec![ + ( + block_number, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles1), + node: Some(node1_old), // Value from overlay, not DB + } + ), + ( + block_number, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles2), + node: None, + } + ), + ] + ); + } + + // Verify storage changesets were written correctly + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_read::().unwrap(); + + // Check normal storage trie changesets + let key1 = BlockNumberHashedAddress((block_number, storage_address1)); + let entries1 = + cursor.walk_dup(Some(key1), None).unwrap().collect::, _>>().unwrap(); + + assert_eq!( + entries1, + vec![ + ( + key1, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: Some(storage_node1_old), // Old value from overlay + } + ), + ( + key1, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles2), + node: None, // New node, no previous value + } + ), + ] + ); + + // Check wiped storage trie changesets + let key2 = BlockNumberHashedAddress((block_number, storage_address2)); + let entries2 = + cursor.walk_dup(Some(key2), None).unwrap().collect::, _>>().unwrap(); + + assert_eq!( + entries2, + vec![ + ( + key2, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: Some(storage_node1), // Value from overlay + } + ), + ( + key2, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles2), + node: None, // Was not in overlay + } + ), + ( + key2, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles3), + node: Some(storage_node2), /* Existing node from overlay in wiped + * storage */ + } + ), + ] + ); + } + + provider_rw.commit().unwrap(); + } + + #[test] + fn test_clear_trie_changesets_from() { + use alloy_primitives::hex_literal::hex; + use reth_db_api::models::BlockNumberHashedAddress; + use reth_trie::{BranchNodeCompact, StoredNibblesSubKey, TrieChangeSetsEntry}; + + let factory = create_test_provider_factory(); + + // Create some test data for different block numbers + let block1 = 100u64; + let block2 = 101u64; + let block3 = 102u64; + let block4 = 103u64; + let block5 = 104u64; + + // Create test addresses for storage changesets + let storage_address1 = + B256::from(hex!("1111111111111111111111111111111111111111111111111111111111111111")); + let storage_address2 = + B256::from(hex!("2222222222222222222222222222222222222222222222222222222222222222")); + + // Create test nibbles + let nibbles1 = StoredNibblesSubKey(Nibbles::from_nibbles([0x1, 0x2, 0x3])); + let nibbles2 = StoredNibblesSubKey(Nibbles::from_nibbles([0x4, 0x5, 0x6])); + let nibbles3 = StoredNibblesSubKey(Nibbles::from_nibbles([0x7, 0x8, 0x9])); + + // Create test nodes + let node1 = BranchNodeCompact::new( + 0b1111_1111_1111_1111, + 0b1111_1111_1111_1111, + 0b0000_0000_0000_0001, + vec![B256::from(hex!( + "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ))], + None, + ); + let node2 = BranchNodeCompact::new( + 0b1111_1111_1111_1110, + 0b1111_1111_1111_1110, + 0b0000_0000_0000_0010, + vec![B256::from(hex!( + "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" + ))], + Some(B256::from(hex!( + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ))), + ); + + // Populate AccountsTrieChangeSets with data across multiple blocks + { + let provider_rw = factory.provider_rw().unwrap(); + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + + // Block 100: 2 entries (will be kept - before start block) + cursor + .upsert( + block1, + &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node1.clone()) }, + ) + .unwrap(); + cursor + .upsert(block1, &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: None }) + .unwrap(); + + // Block 101: 3 entries with duplicates (will be deleted - from this block onwards) + cursor + .upsert( + block2, + &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node2.clone()) }, + ) + .unwrap(); + cursor + .upsert( + block2, + &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node1.clone()) }, + ) + .unwrap(); // duplicate key + cursor + .upsert(block2, &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: None }) + .unwrap(); + + // Block 102: 2 entries (will be deleted - after start block) + cursor + .upsert( + block3, + &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node1.clone()) }, + ) + .unwrap(); + cursor + .upsert( + block3, + &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: Some(node2.clone()) }, + ) + .unwrap(); + + // Block 103: 1 entry (will be deleted - after start block) + cursor + .upsert(block4, &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: None }) + .unwrap(); + + // Block 104: 2 entries (will be deleted - after start block) + cursor + .upsert( + block5, + &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node2.clone()) }, + ) + .unwrap(); + cursor + .upsert(block5, &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: None }) + .unwrap(); + + provider_rw.commit().unwrap(); + } + + // Populate StoragesTrieChangeSets with data across multiple blocks + { + let provider_rw = factory.provider_rw().unwrap(); + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + + // Block 100, address1: 2 entries (will be kept - before start block) + let key1_block1 = BlockNumberHashedAddress((block1, storage_address1)); + cursor + .upsert( + key1_block1, + &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node1.clone()) }, + ) + .unwrap(); + cursor + .upsert(key1_block1, &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: None }) + .unwrap(); + + // Block 101, address1: 3 entries with duplicates (will be deleted - from this block + // onwards) + let key1_block2 = BlockNumberHashedAddress((block2, storage_address1)); + cursor + .upsert( + key1_block2, + &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: Some(node2.clone()) }, + ) + .unwrap(); + cursor + .upsert(key1_block2, &TrieChangeSetsEntry { nibbles: nibbles1.clone(), node: None }) + .unwrap(); // duplicate key + cursor + .upsert( + key1_block2, + &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node1.clone()) }, + ) + .unwrap(); + + // Block 102, address2: 2 entries (will be deleted - after start block) + let key2_block3 = BlockNumberHashedAddress((block3, storage_address2)); + cursor + .upsert( + key2_block3, + &TrieChangeSetsEntry { nibbles: nibbles2.clone(), node: Some(node2.clone()) }, + ) + .unwrap(); + cursor + .upsert(key2_block3, &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: None }) + .unwrap(); + + // Block 103, address1: 2 entries with duplicate (will be deleted - after start block) + let key1_block4 = BlockNumberHashedAddress((block4, storage_address1)); + cursor + .upsert( + key1_block4, + &TrieChangeSetsEntry { nibbles: nibbles3.clone(), node: Some(node1) }, + ) + .unwrap(); + cursor + .upsert( + key1_block4, + &TrieChangeSetsEntry { nibbles: nibbles3, node: Some(node2.clone()) }, + ) + .unwrap(); // duplicate key + + // Block 104, address2: 2 entries (will be deleted - after start block) + let key2_block5 = BlockNumberHashedAddress((block5, storage_address2)); + cursor + .upsert(key2_block5, &TrieChangeSetsEntry { nibbles: nibbles1, node: None }) + .unwrap(); + cursor + .upsert(key2_block5, &TrieChangeSetsEntry { nibbles: nibbles2, node: Some(node2) }) + .unwrap(); + + provider_rw.commit().unwrap(); + } + + // Clear all changesets from block 101 onwards + { + let provider_rw = factory.provider_rw().unwrap(); + provider_rw.clear_trie_changesets_from(block2).unwrap(); + provider_rw.commit().unwrap(); + } + + // Verify AccountsTrieChangeSets after clearing + { + let provider = factory.provider().unwrap(); + let mut cursor = + provider.tx_ref().cursor_dup_read::().unwrap(); + + // Block 100 should still exist (before range) + let block1_entries = cursor + .walk_dup(Some(block1), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(block1_entries.len(), 2, "Block 100 entries should be preserved"); + assert_eq!(block1_entries[0].0, block1); + assert_eq!(block1_entries[1].0, block1); + + // Blocks 101-104 should be deleted + let block2_entries = cursor + .walk_dup(Some(block2), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block2_entries.is_empty(), "Block 101 entries should be deleted"); + + let block3_entries = cursor + .walk_dup(Some(block3), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block3_entries.is_empty(), "Block 102 entries should be deleted"); + + let block4_entries = cursor + .walk_dup(Some(block4), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block4_entries.is_empty(), "Block 103 entries should be deleted"); + + // Block 104 should also be deleted + let block5_entries = cursor + .walk_dup(Some(block5), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block5_entries.is_empty(), "Block 104 entries should be deleted"); + } + + // Verify StoragesTrieChangeSets after clearing + { + let provider = factory.provider().unwrap(); + let mut cursor = + provider.tx_ref().cursor_dup_read::().unwrap(); + + // Block 100 entries should still exist (before range) + let key1_block1 = BlockNumberHashedAddress((block1, storage_address1)); + let block1_entries = cursor + .walk_dup(Some(key1_block1), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(block1_entries.len(), 2, "Block 100 storage entries should be preserved"); + + // Blocks 101-104 entries should be deleted + let key1_block2 = BlockNumberHashedAddress((block2, storage_address1)); + let block2_entries = cursor + .walk_dup(Some(key1_block2), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block2_entries.is_empty(), "Block 101 storage entries should be deleted"); + + let key2_block3 = BlockNumberHashedAddress((block3, storage_address2)); + let block3_entries = cursor + .walk_dup(Some(key2_block3), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block3_entries.is_empty(), "Block 102 storage entries should be deleted"); + + let key1_block4 = BlockNumberHashedAddress((block4, storage_address1)); + let block4_entries = cursor + .walk_dup(Some(key1_block4), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block4_entries.is_empty(), "Block 103 storage entries should be deleted"); + + // Block 104 entries should also be deleted + let key2_block5 = BlockNumberHashedAddress((block5, storage_address2)); + let block5_entries = cursor + .walk_dup(Some(key2_block5), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(block5_entries.is_empty(), "Block 104 storage entries should be deleted"); + } + } + + #[test] + fn test_write_trie_updates_sorted() { + use reth_trie::{ + updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted}, + BranchNodeCompact, StorageTrieEntry, + }; + + let factory = create_test_provider_factory(); + let provider_rw = factory.provider_rw().unwrap(); + + // Pre-populate account trie with data that will be deleted + { + let tx = provider_rw.tx_ref(); + let mut cursor = tx.cursor_write::().unwrap(); + + // Add account node that will be deleted + let to_delete = StoredNibbles(Nibbles::from_nibbles([0x3, 0x4])); + cursor + .upsert( + to_delete, + &BranchNodeCompact::new( + 0b1010_1010_1010_1010, // state_mask + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask + vec![], + None, + ), + ) + .unwrap(); + + // Add account node that will be updated + let to_update = StoredNibbles(Nibbles::from_nibbles([0x1, 0x2])); + cursor + .upsert( + to_update, + &BranchNodeCompact::new( + 0b0101_0101_0101_0101, // old state_mask (will be updated) + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask + vec![], + None, + ), + ) + .unwrap(); + } + + // Pre-populate storage tries with data + let storage_address1 = B256::from([1u8; 32]); + let storage_address2 = B256::from([2u8; 32]); + { + let tx = provider_rw.tx_ref(); + let mut storage_cursor = tx.cursor_dup_write::().unwrap(); + + // Add storage nodes for address1 (one will be deleted) + storage_cursor + .upsert( + storage_address1, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(Nibbles::from_nibbles([0x2, 0x0])), + node: BranchNodeCompact::new( + 0b0011_0011_0011_0011, // will be deleted + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ), + }, + ) + .unwrap(); + + // Add storage nodes for address2 (will be wiped) + storage_cursor + .upsert( + storage_address2, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(Nibbles::from_nibbles([0xa, 0xb])), + node: BranchNodeCompact::new( + 0b1100_1100_1100_1100, // will be wiped + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ), + }, + ) + .unwrap(); + storage_cursor + .upsert( + storage_address2, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(Nibbles::from_nibbles([0xc, 0xd])), + node: BranchNodeCompact::new( + 0b0011_1100_0011_1100, // will be wiped + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ), + }, + ) + .unwrap(); + } + + // Create sorted account trie updates + let account_nodes = vec![ + ( + Nibbles::from_nibbles([0x1, 0x2]), + Some(BranchNodeCompact::new( + 0b1111_1111_1111_1111, // state_mask (updated) + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask (no hashes) + vec![], + None, + )), + ), + (Nibbles::from_nibbles([0x3, 0x4]), None), // Deletion + ( + Nibbles::from_nibbles([0x5, 0x6]), + Some(BranchNodeCompact::new( + 0b1111_1111_1111_1111, // state_mask + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask (no hashes) + vec![], + None, + )), + ), + ]; + + // Create sorted storage trie updates + let storage_trie1 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + ( + Nibbles::from_nibbles([0x1, 0x0]), + Some(BranchNodeCompact::new( + 0b1111_0000_0000_0000, // state_mask + 0b0000_0000_0000_0000, // tree_mask + 0b0000_0000_0000_0000, // hash_mask (no hashes) + vec![], + None, + )), + ), + (Nibbles::from_nibbles([0x2, 0x0]), None), // Deletion of existing node + ], + }; + + let storage_trie2 = StorageTrieUpdatesSorted { + is_deleted: true, // Wipe all storage for this address + storage_nodes: vec![], + }; + + let mut storage_tries = B256Map::default(); + storage_tries.insert(storage_address1, storage_trie1); + storage_tries.insert(storage_address2, storage_trie2); + + let trie_updates = TrieUpdatesSorted { account_nodes, storage_tries }; + + // Write the sorted trie updates + let num_entries = provider_rw.write_trie_updates_sorted(&trie_updates).unwrap(); + + // We should have 2 account insertions + 1 account deletion + 1 storage insertion + 1 + // storage deletion = 5 + assert_eq!(num_entries, 5); + + // Verify account trie updates were written correctly + let tx = provider_rw.tx_ref(); + let mut cursor = tx.cursor_read::().unwrap(); + + // Check first account node was updated + let nibbles1 = StoredNibbles(Nibbles::from_nibbles([0x1, 0x2])); + let entry1 = cursor.seek_exact(nibbles1).unwrap(); + assert!(entry1.is_some(), "Updated account node should exist"); + let expected_mask = reth_trie::TrieMask::new(0b1111_1111_1111_1111); + assert_eq!( + entry1.unwrap().1.state_mask, + expected_mask, + "Account node should have updated state_mask" + ); + + // Check deleted account node no longer exists + let nibbles2 = StoredNibbles(Nibbles::from_nibbles([0x3, 0x4])); + let entry2 = cursor.seek_exact(nibbles2).unwrap(); + assert!(entry2.is_none(), "Deleted account node should not exist"); + + // Check new account node exists + let nibbles3 = StoredNibbles(Nibbles::from_nibbles([0x5, 0x6])); + let entry3 = cursor.seek_exact(nibbles3).unwrap(); + assert!(entry3.is_some(), "New account node should exist"); + + // Verify storage trie updates were written correctly + let mut storage_cursor = tx.cursor_dup_read::().unwrap(); + + // Check storage for address1 + let storage_entries1: Vec<_> = storage_cursor + .walk_dup(Some(storage_address1), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!( + storage_entries1.len(), + 1, + "Storage address1 should have 1 entry after deletion" + ); + assert_eq!( + storage_entries1[0].1.nibbles.0, + Nibbles::from_nibbles([0x1, 0x0]), + "Remaining entry should be [0x1, 0x0]" + ); + + // Check storage for address2 was wiped + let storage_entries2: Vec<_> = storage_cursor + .walk_dup(Some(storage_address2), None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert_eq!(storage_entries2.len(), 0, "Storage address2 should be empty after wipe"); + + provider_rw.commit().unwrap(); + } + + #[test] + fn test_get_block_trie_updates() { + use reth_db_api::models::BlockNumberHashedAddress; + use reth_trie::{BranchNodeCompact, StorageTrieEntry}; + + let factory = create_test_provider_factory(); + let provider_rw = factory.provider_rw().unwrap(); + + let target_block = 2u64; + let next_block = 3u64; + + // Create test nibbles and nodes for accounts + let account_nibbles1 = Nibbles::from_nibbles([0x1, 0x2, 0x3, 0x4]); + let account_nibbles2 = Nibbles::from_nibbles([0x5, 0x6, 0x7, 0x8]); + let account_nibbles3 = Nibbles::from_nibbles([0x9, 0xa, 0xb, 0xc]); + + let node1 = BranchNodeCompact::new( + 0b1111_1111_0000_0000, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + let node2 = BranchNodeCompact::new( + 0b0000_0000_1111_1111, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + let node3 = BranchNodeCompact::new( + 0b1010_1010_1010_1010, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Pre-populate AccountsTrie with nodes that will be the final state + { + let mut cursor = provider_rw.tx_ref().cursor_write::().unwrap(); + cursor.insert(StoredNibbles(account_nibbles1), &node1).unwrap(); + cursor.insert(StoredNibbles(account_nibbles2), &node2).unwrap(); + // account_nibbles3 will be deleted (not in final state) + } + + // Insert trie changesets for target_block + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + // nibbles1 was updated in target_block (old value stored) + cursor + .append_dup( + target_block, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles1), + node: Some(BranchNodeCompact::new( + 0b1111_0000_0000_0000, // old value + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + )), + }, + ) + .unwrap(); + // nibbles2 was created in target_block (no old value) + cursor + .append_dup( + target_block, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles2), + node: None, + }, + ) + .unwrap(); + } + + // Insert trie changesets for next_block (to test overlay) + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + // nibbles3 was deleted in next_block (old value stored) + cursor + .append_dup( + next_block, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(account_nibbles3), + node: Some(node3), + }, + ) + .unwrap(); + } + + // Storage trie updates + let storage_address1 = B256::from([1u8; 32]); + let storage_nibbles1 = Nibbles::from_nibbles([0xa, 0xb]); + let storage_nibbles2 = Nibbles::from_nibbles([0xc, 0xd]); + + let storage_node1 = BranchNodeCompact::new( + 0b1111_1111_1111_0000, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + let storage_node2 = BranchNodeCompact::new( + 0b0101_0101_0101_0101, + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + ); + + // Pre-populate StoragesTrie with final state + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + cursor + .upsert( + storage_address1, + &StorageTrieEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: storage_node1.clone(), + }, + ) + .unwrap(); + // storage_nibbles2 was deleted in next_block, so it's not in final state + } + + // Insert storage trie changesets for target_block + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + let key = BlockNumberHashedAddress((target_block, storage_address1)); + + // storage_nibbles1 was updated + cursor + .append_dup( + key, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles1), + node: Some(BranchNodeCompact::new( + 0b0000_0000_1111_1111, // old value + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + )), + }, + ) + .unwrap(); + + // storage_nibbles2 was created + cursor + .append_dup( + key, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles2), + node: None, + }, + ) + .unwrap(); + } + + // Insert storage trie changesets for next_block (to test overlay) + { + let mut cursor = + provider_rw.tx_ref().cursor_dup_write::().unwrap(); + let key = BlockNumberHashedAddress((next_block, storage_address1)); + + // storage_nibbles2 was deleted in next_block + cursor + .append_dup( + key, + TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey(storage_nibbles2), + node: Some(BranchNodeCompact::new( + 0b0101_0101_0101_0101, // value that was deleted + 0b0000_0000_0000_0000, + 0b0000_0000_0000_0000, + vec![], + None, + )), + }, + ) + .unwrap(); + } + + provider_rw.commit().unwrap(); + + // Now test get_block_trie_updates + let provider = factory.provider().unwrap(); + let result = provider.get_block_trie_updates(target_block).unwrap(); + + // Verify account trie updates + assert_eq!(result.account_nodes.len(), 2, "Should have 2 account trie updates"); + + // Check nibbles1 - should have the current value (node1) + let nibbles1_update = result + .account_nodes + .iter() + .find(|(n, _)| n == &account_nibbles1) + .expect("Should find nibbles1"); + assert!(nibbles1_update.1.is_some(), "nibbles1 should have a value"); + assert_eq!( + nibbles1_update.1.as_ref().unwrap().state_mask, + node1.state_mask, + "nibbles1 should have current value" + ); + + // Check nibbles2 - should have the current value (node2) + let nibbles2_update = result + .account_nodes + .iter() + .find(|(n, _)| n == &account_nibbles2) + .expect("Should find nibbles2"); + assert!(nibbles2_update.1.is_some(), "nibbles2 should have a value"); + assert_eq!( + nibbles2_update.1.as_ref().unwrap().state_mask, + node2.state_mask, + "nibbles2 should have current value" + ); + + // nibbles3 should NOT be in the result (it was changed in next_block, not target_block) + assert!( + !result.account_nodes.iter().any(|(n, _)| n == &account_nibbles3), + "nibbles3 should not be in target_block updates" + ); + + // Verify storage trie updates + assert_eq!(result.storage_tries.len(), 1, "Should have 1 storage trie"); + let storage_updates = result + .storage_tries + .get(&storage_address1) + .expect("Should have storage updates for address1"); + + assert_eq!(storage_updates.storage_nodes.len(), 2, "Should have 2 storage node updates"); + + // Check storage_nibbles1 - should have current value + let storage1_update = storage_updates + .storage_nodes + .iter() + .find(|(n, _)| n == &storage_nibbles1) + .expect("Should find storage_nibbles1"); + assert!(storage1_update.1.is_some(), "storage_nibbles1 should have a value"); + assert_eq!( + storage1_update.1.as_ref().unwrap().state_mask, + storage_node1.state_mask, + "storage_nibbles1 should have current value" + ); + + // Check storage_nibbles2 - was created in target_block, will be deleted in next_block + // So it should have a value (the value that will be deleted) + let storage2_update = storage_updates + .storage_nodes + .iter() + .find(|(n, _)| n == &storage_nibbles2) + .expect("Should find storage_nibbles2"); + assert!( + storage2_update.1.is_some(), + "storage_nibbles2 should have a value (the node that will be deleted in next block)" + ); + assert_eq!( + storage2_update.1.as_ref().unwrap().state_mask, + storage_node2.state_mask, + "storage_nibbles2 should have the value that was created and will be deleted" + ); + } } diff --git a/crates/storage/provider/src/providers/mod.rs b/crates/storage/provider/src/providers/mod.rs index ab54fe01e56..5a950bbd7d2 100644 --- a/crates/storage/provider/src/providers/mod.rs +++ b/crates/storage/provider/src/providers/mod.rs @@ -17,7 +17,7 @@ mod state; pub use state::{ historical::{HistoricalStateProvider, HistoricalStateProviderRef, LowestAvailableBlocks}, latest::{LatestStateProvider, LatestStateProviderRef}, - overlay::OverlayStateProvider, + overlay::{OverlayStateProvider, OverlayStateProviderFactory}, }; mod consistent_view; diff --git a/crates/storage/provider/src/providers/state/overlay.rs b/crates/storage/provider/src/providers/state/overlay.rs index 7e6a40efef2..71c1a693193 100644 --- a/crates/storage/provider/src/providers/state/overlay.rs +++ b/crates/storage/provider/src/providers/state/overlay.rs @@ -1,15 +1,143 @@ -use alloy_primitives::B256; +use alloy_primitives::{BlockNumber, B256}; use reth_db_api::DatabaseError; -use reth_storage_api::DBProvider; +use reth_errors::ProviderError; +use reth_stages_types::StageId; +use reth_storage_api::{DBProvider, DatabaseProviderFactory, StageCheckpointReader, TrieReader}; use reth_trie::{ hashed_cursor::{HashedCursorFactory, HashedPostStateCursorFactory}, trie_cursor::{InMemoryTrieCursorFactory, TrieCursorFactory}, updates::TrieUpdatesSorted, - HashedPostStateSorted, + HashedPostState, HashedPostStateSorted, KeccakKeyHasher, +}; +use reth_trie_db::{ + DatabaseHashedCursorFactory, DatabaseHashedPostState, DatabaseTrieCursorFactory, }; -use reth_trie_db::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory}; use std::sync::Arc; +/// Factory for creating overlay state providers with optional reverts and overlays. +/// +/// This factory allows building an `OverlayStateProvider` whose DB state has been reverted to a +/// particular block, and/or with additional overlay information added on top. +#[derive(Debug, Clone)] +pub struct OverlayStateProviderFactory { + /// The underlying database provider factory + factory: F, + /// Optional block number for collecting reverts + block_number: Option, + /// Optional trie overlay + trie_overlay: Option>, + /// Optional hashed state overlay + hashed_state_overlay: Option>, +} + +impl OverlayStateProviderFactory +where + F: DatabaseProviderFactory, + F::Provider: Clone + TrieReader + StageCheckpointReader, +{ + /// Create a new overlay state provider factory + pub const fn new(factory: F) -> Self { + Self { factory, block_number: None, trie_overlay: None, hashed_state_overlay: None } + } + + /// Set the block number for collecting reverts + pub const fn with_block_number(mut self, block_number: Option) -> Self { + self.block_number = block_number; + self + } + + /// Set the trie overlay + pub fn with_trie_overlay(mut self, trie_overlay: Option>) -> Self { + self.trie_overlay = trie_overlay; + self + } + + /// Set the hashed state overlay + pub fn with_hashed_state_overlay( + mut self, + hashed_state_overlay: Option>, + ) -> Self { + self.hashed_state_overlay = hashed_state_overlay; + self + } + + /// Validates that there are sufficient changesets to revert to the requested block number. + /// + /// Returns an error if the `MerkleChangeSets` checkpoint doesn't cover the requested block. + fn validate_changesets_availability( + &self, + provider: &F::Provider, + requested_block: BlockNumber, + ) -> Result<(), ProviderError> { + // Get the MerkleChangeSets stage checkpoint - let errors propagate as-is + let checkpoint = provider.get_stage_checkpoint(StageId::MerkleChangeSets)?; + + // If there's no checkpoint at all or block range details are missing, we can't revert + let available_range = checkpoint + .and_then(|chk| { + chk.merkle_changesets_stage_checkpoint() + .map(|stage_chk| stage_chk.block_range.from..=chk.block_number) + }) + .ok_or_else(|| ProviderError::InsufficientChangesets { + requested: requested_block, + available: 0..=0, + })?; + + // Check if the requested block is within the available range + if !available_range.contains(&requested_block) { + return Err(ProviderError::InsufficientChangesets { + requested: requested_block, + available: available_range, + }); + } + + Ok(()) + } + + /// Create a read-only [`OverlayStateProvider`]. + pub fn provider_ro(&self) -> Result, ProviderError> { + // Get a read-only provider + let provider = self.factory.database_provider_ro()?; + + // If block_number is provided, collect reverts + let (trie_updates, hashed_state) = if let Some(from_block) = self.block_number { + // Validate that we have sufficient changesets for the requested block + self.validate_changesets_availability(&provider, from_block)?; + + // Collect trie reverts + let mut trie_updates_mut = provider.trie_reverts(from_block)?; + + // Collect state reverts using HashedPostState::from_reverts + let reverted_state = + HashedPostState::from_reverts::(provider.tx_ref(), from_block..)?; + let mut hashed_state_mut = reverted_state.into_sorted(); + + // Extend with overlays if provided + if let Some(trie_overlay) = &self.trie_overlay { + trie_updates_mut.extend_ref(trie_overlay); + } + + if let Some(hashed_state_overlay) = &self.hashed_state_overlay { + hashed_state_mut.extend_ref(hashed_state_overlay); + } + + (Arc::new(trie_updates_mut), Arc::new(hashed_state_mut)) + } else { + // If no block_number, use overlays directly or defaults + let trie_updates = + self.trie_overlay.clone().unwrap_or_else(|| Arc::new(TrieUpdatesSorted::default())); + let hashed_state = self + .hashed_state_overlay + .clone() + .unwrap_or_else(|| Arc::new(HashedPostStateSorted::default())); + + (trie_updates, hashed_state) + }; + + Ok(OverlayStateProvider::new(provider, trie_updates, hashed_state)) + } +} + /// State provider with in-memory overlay from trie updates and hashed post state. /// /// This provider uses in-memory trie updates and hashed post state as an overlay diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index 1024312ead9..3e33e2b0509 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -34,12 +34,13 @@ use reth_stages_types::{StageCheckpoint, StageId}; use reth_storage_api::{ BlockBodyIndicesProvider, BytecodeReader, DBProvider, DatabaseProviderFactory, HashedPostStateProvider, NodePrimitivesProvider, StageCheckpointReader, StateProofProvider, - StorageRootProvider, + StorageRootProvider, TrieReader, }; use reth_storage_errors::provider::{ConsistentViewError, ProviderError, ProviderResult}; use reth_trie::{ - updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof, - MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, + updates::{TrieUpdates, TrieUpdatesSorted}, + AccountProof, HashedPostState, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, + StorageProof, TrieInput, }; use std::{ collections::BTreeMap, @@ -1005,6 +1006,19 @@ impl StateReader for MockEthProvider< } } +impl TrieReader for MockEthProvider { + fn trie_reverts(&self, _from: BlockNumber) -> ProviderResult { + Ok(TrieUpdatesSorted::default()) + } + + fn get_block_trie_updates( + &self, + _block_number: BlockNumber, + ) -> ProviderResult { + Ok(TrieUpdatesSorted::default()) + } +} + impl CanonStateSubscriptions for MockEthProvider { diff --git a/crates/storage/provider/src/test_utils/mod.rs b/crates/storage/provider/src/test_utils/mod.rs index d65655de8bf..ccda2d60e85 100644 --- a/crates/storage/provider/src/test_utils/mod.rs +++ b/crates/storage/provider/src/test_utils/mod.rs @@ -89,7 +89,7 @@ pub fn insert_genesis>( let (root, updates) = StateRoot::from_tx(provider.tx_ref()) .root_with_updates() .map_err(reth_db::DatabaseError::from)?; - provider.write_trie_updates(&updates).unwrap(); + provider.write_trie_updates(updates).unwrap(); provider.commit()?; diff --git a/crates/storage/provider/src/traits/full.rs b/crates/storage/provider/src/traits/full.rs index 374a35f473c..710ca9400ed 100644 --- a/crates/storage/provider/src/traits/full.rs +++ b/crates/storage/provider/src/traits/full.rs @@ -3,7 +3,7 @@ use crate::{ AccountReader, BlockReader, BlockReaderIdExt, ChainSpecProvider, ChangeSetReader, DatabaseProviderFactory, HashedPostStateProvider, StageCheckpointReader, StateProviderFactory, - StateReader, StaticFileProviderFactory, + StateReader, StaticFileProviderFactory, TrieReader, }; use reth_chain_state::{CanonStateSubscriptions, ForkChoiceSubscriptions}; use reth_node_types::{BlockTy, HeaderTy, NodeTypesWithDB, ReceiptTy, TxTy}; @@ -12,7 +12,7 @@ use std::fmt::Debug; /// Helper trait to unify all provider traits for simplicity. pub trait FullProvider: - DatabaseProviderFactory + DatabaseProviderFactory + NodePrimitivesProvider + StaticFileProviderFactory + BlockReaderIdExt< @@ -37,7 +37,7 @@ pub trait FullProvider: } impl FullProvider for T where - T: DatabaseProviderFactory + T: DatabaseProviderFactory + NodePrimitivesProvider + StaticFileProviderFactory + BlockReaderIdExt< diff --git a/crates/storage/provider/src/writer/mod.rs b/crates/storage/provider/src/writer/mod.rs index 1151990f97b..6d990e17a49 100644 --- a/crates/storage/provider/src/writer/mod.rs +++ b/crates/storage/provider/src/writer/mod.rs @@ -909,7 +909,7 @@ mod tests { } let (_, updates) = StateRoot::from_tx(tx).root_with_updates().unwrap(); - provider_rw.write_trie_updates(&updates).unwrap(); + provider_rw.write_trie_updates(updates).unwrap(); let mut state = State::builder().with_bundle_update().build(); @@ -1127,7 +1127,10 @@ mod tests { assert_eq!(storage_root, storage_root_prehashed(init_storage.storage)); assert!(!storage_updates.is_empty()); provider_rw - .write_storage_trie_updates(core::iter::once((&hashed_address, &storage_updates))) + .write_storage_trie_updates_sorted(core::iter::once(( + &hashed_address, + &storage_updates.into_sorted(), + ))) .unwrap(); // destroy the storage and re-create with new slots diff --git a/crates/storage/storage-api/src/noop.rs b/crates/storage/storage-api/src/noop.rs index e0c57d5226b..6b70a5260a6 100644 --- a/crates/storage/storage-api/src/noop.rs +++ b/crates/storage/storage-api/src/noop.rs @@ -6,7 +6,7 @@ use crate::{ HashedPostStateProvider, HeaderProvider, NodePrimitivesProvider, PruneCheckpointReader, ReceiptProvider, ReceiptProviderIdExt, StageCheckpointReader, StateProofProvider, StateProvider, StateProviderBox, StateProviderFactory, StateReader, StateRootProvider, - StorageRootProvider, TransactionVariant, TransactionsProvider, + StorageRootProvider, TransactionVariant, TransactionsProvider, TrieReader, }; #[cfg(feature = "db-api")] @@ -35,8 +35,9 @@ use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_storage_errors::provider::{ProviderError, ProviderResult}; use reth_trie_common::{ - updates::TrieUpdates, AccountProof, HashedPostState, HashedStorage, MultiProof, - MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, + updates::{TrieUpdates, TrieUpdatesSorted}, + AccountProof, HashedPostState, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, + StorageProof, TrieInput, }; /// Supports various api interfaces for testing purposes. @@ -59,7 +60,7 @@ impl NoopProvider { #[cfg(feature = "db-api")] tx: TxMock::default(), #[cfg(feature = "db-api")] - prune_modes: PruneModes::none(), + prune_modes: PruneModes::default(), _phantom: Default::default(), } } @@ -73,7 +74,7 @@ impl NoopProvider { #[cfg(feature = "db-api")] tx: TxMock::default(), #[cfg(feature = "db-api")] - prune_modes: PruneModes::none(), + prune_modes: PruneModes::default(), _phantom: Default::default(), } } @@ -646,6 +647,19 @@ impl DBProvider for NoopProvider TrieReader for NoopProvider { + fn trie_reverts(&self, _from: BlockNumber) -> ProviderResult { + Ok(TrieUpdatesSorted::default()) + } + + fn get_block_trie_updates( + &self, + _block_number: BlockNumber, + ) -> ProviderResult { + Ok(TrieUpdatesSorted::default()) + } +} + #[cfg(feature = "db-api")] impl DatabaseProviderFactory for NoopProvider diff --git a/crates/storage/storage-api/src/trie.rs b/crates/storage/storage-api/src/trie.rs index 3f39cf3838d..9ff02c106e5 100644 --- a/crates/storage/storage-api/src/trie.rs +++ b/crates/storage/storage-api/src/trie.rs @@ -1,8 +1,8 @@ use alloc::vec::Vec; -use alloy_primitives::{Address, Bytes, B256}; +use alloy_primitives::{Address, BlockNumber, Bytes, B256}; use reth_storage_errors::provider::ProviderResult; use reth_trie_common::{ - updates::{StorageTrieUpdates, TrieUpdates}, + updates::{StorageTrieUpdatesSorted, TrieUpdates, TrieUpdatesSorted}, AccountProof, HashedPostState, HashedStorage, MultiProof, MultiProofTargets, StorageMultiProof, StorageProof, TrieInput, }; @@ -89,25 +89,93 @@ pub trait StateProofProvider: Send + Sync { fn witness(&self, input: TrieInput, target: HashedPostState) -> ProviderResult>; } +/// Trie Reader +#[auto_impl::auto_impl(&, Arc, Box)] +pub trait TrieReader: Send + Sync { + /// Returns the [`TrieUpdatesSorted`] for reverting the trie database to its state prior to the + /// given block and onwards having been processed. + fn trie_reverts(&self, from: BlockNumber) -> ProviderResult; + + /// Returns the trie updates that were applied by the specified block. + fn get_block_trie_updates( + &self, + block_number: BlockNumber, + ) -> ProviderResult; +} + /// Trie Writer #[auto_impl::auto_impl(&, Arc, Box)] pub trait TrieWriter: Send + Sync { /// Writes trie updates to the database. /// /// Returns the number of entries modified. - fn write_trie_updates(&self, trie_updates: &TrieUpdates) -> ProviderResult; + fn write_trie_updates(&self, trie_updates: TrieUpdates) -> ProviderResult { + self.write_trie_updates_sorted(&trie_updates.into_sorted()) + } + + /// Writes trie updates to the database with already sorted updates. + /// + /// Returns the number of entries modified. + fn write_trie_updates_sorted(&self, trie_updates: &TrieUpdatesSorted) -> ProviderResult; + + /// Records the current values of all trie nodes which will be updated using the [`TrieUpdates`] + /// into the trie changesets tables. + /// + /// The intended usage of this method is to call it _prior_ to calling `write_trie_updates` with + /// the same [`TrieUpdates`]. + /// + /// The `updates_overlay` parameter allows providing additional in-memory trie updates that + /// should be considered when looking up current node values. When provided, these overlay + /// updates are applied on top of the database state, allowing the method to see a view that + /// includes both committed database values and pending in-memory changes. This is useful + /// when writing changesets for updates that depend on previous uncommitted trie changes. + /// + /// Returns the number of keys written. + fn write_trie_changesets( + &self, + block_number: BlockNumber, + trie_updates: &TrieUpdatesSorted, + updates_overlay: Option<&TrieUpdatesSorted>, + ) -> ProviderResult; + + /// Clears contents of trie changesets completely + fn clear_trie_changesets(&self) -> ProviderResult<()>; + + /// Clears contents of trie changesets starting from the given block number (inclusive) onwards. + fn clear_trie_changesets_from(&self, from: BlockNumber) -> ProviderResult<()>; } /// Storage Trie Writer #[auto_impl::auto_impl(&, Arc, Box)] pub trait StorageTrieWriter: Send + Sync { - /// Writes storage trie updates from the given storage trie map. + /// Writes storage trie updates from the given storage trie map with already sorted updates. /// - /// First sorts the storage trie updates by the hashed address key, writing in sorted order. + /// Expects the storage trie updates to already be sorted by the hashed address key. /// /// Returns the number of entries modified. - fn write_storage_trie_updates<'a>( + fn write_storage_trie_updates_sorted<'a>( + &self, + storage_tries: impl Iterator, + ) -> ProviderResult; + + /// Records the current values of all trie nodes which will be updated using the + /// [`StorageTrieUpdatesSorted`] into the storage trie changesets table. + /// + /// The intended usage of this method is to call it _prior_ to calling + /// `write_storage_trie_updates` with the same set of [`StorageTrieUpdatesSorted`]. + /// + /// The `updates_overlay` parameter allows providing additional in-memory trie updates that + /// should be considered when looking up current node values. When provided, these overlay + /// updates are applied on top of the database state for each storage trie, allowing the + /// method to see a view that includes both committed database values and pending in-memory + /// changes. This is useful when writing changesets for storage updates that depend on + /// previous uncommitted trie changes. + /// + /// Returns the number of keys written. + fn write_storage_trie_changesets<'a>( &self, - storage_tries: impl Iterator, + block_number: BlockNumber, + storage_tries: impl Iterator, + updates_overlay: Option<&TrieUpdatesSorted>, ) -> ProviderResult; } diff --git a/crates/trie/common/src/hashed_state.rs b/crates/trie/common/src/hashed_state.rs index 50d9f20af0b..27c2807ad2a 100644 --- a/crates/trie/common/src/hashed_state.rs +++ b/crates/trie/common/src/hashed_state.rs @@ -3,6 +3,7 @@ use core::ops::Not; use crate::{ added_removed_keys::MultiAddedRemovedKeys, prefix_set::{PrefixSetMut, TriePrefixSetsMut}, + utils::extend_sorted_vec, KeyHasher, MultiProofTargets, Nibbles, }; use alloc::{borrow::Cow, vec::Vec}; @@ -484,6 +485,21 @@ impl HashedPostStateSorted { pub const fn account_storages(&self) -> &B256Map { &self.storages } + + /// Extends this state with contents of another sorted state. + /// Entries in `other` take precedence for duplicate keys. + pub fn extend_ref(&mut self, other: &Self) { + // Extend accounts + self.accounts.extend_ref(&other.accounts); + + // Extend storages + for (hashed_address, other_storage) in &other.storages { + self.storages + .entry(*hashed_address) + .and_modify(|existing| existing.extend_ref(other_storage)) + .or_insert_with(|| other_storage.clone()); + } + } } impl AsRef for HashedPostStateSorted { @@ -510,6 +526,20 @@ impl HashedAccountsSorted { .chain(self.destroyed_accounts.iter().map(|address| (*address, None))) .sorted_by_key(|entry| *entry.0) } + + /// Extends this collection with contents of another sorted collection. + /// Entries in `other` take precedence for duplicate keys. + pub fn extend_ref(&mut self, other: &Self) { + // Updates take precedence over removals, so we want removals from `other` to only apply to + // the previous accounts. + self.accounts.retain(|(addr, _)| !other.destroyed_accounts.contains(addr)); + + // Extend the sorted accounts vector + extend_sorted_vec(&mut self.accounts, &other.accounts); + + // Merge destroyed accounts sets + self.destroyed_accounts.extend(&other.destroyed_accounts); + } } /// Sorted hashed storage optimized for iterating during state trie calculation. @@ -537,6 +567,28 @@ impl HashedStorageSorted { .chain(self.zero_valued_slots.iter().map(|hashed_slot| (*hashed_slot, U256::ZERO))) .sorted_by_key(|entry| *entry.0) } + + /// Extends this storage with contents of another sorted storage. + /// Entries in `other` take precedence for duplicate keys. + pub fn extend_ref(&mut self, other: &Self) { + if other.wiped { + // If other is wiped, clear everything and copy from other + self.wiped = true; + self.non_zero_valued_slots.clear(); + self.zero_valued_slots.clear(); + self.non_zero_valued_slots.extend_from_slice(&other.non_zero_valued_slots); + self.zero_valued_slots.extend(&other.zero_valued_slots); + return; + } + + self.non_zero_valued_slots.retain(|(slot, _)| !other.zero_valued_slots.contains(slot)); + + // Extend the sorted non-zero valued slots + extend_sorted_vec(&mut self.non_zero_valued_slots, &other.non_zero_valued_slots); + + // Merge zero valued slots sets + self.zero_valued_slots.extend(&other.zero_valued_slots); + } } /// An iterator that yields chunks of the state updates of at most `size` account and storage @@ -1072,4 +1124,102 @@ mod tests { ); assert_eq!(chunks.next(), None); } + + #[test] + fn test_hashed_post_state_sorted_extend_ref() { + // Test extending accounts + let mut state1 = HashedPostStateSorted { + accounts: HashedAccountsSorted { + accounts: vec![ + (B256::from([1; 32]), Account::default()), + (B256::from([3; 32]), Account::default()), + ], + destroyed_accounts: B256Set::from_iter([B256::from([5; 32])]), + }, + storages: B256Map::default(), + }; + + let state2 = HashedPostStateSorted { + accounts: HashedAccountsSorted { + accounts: vec![ + (B256::from([2; 32]), Account::default()), + (B256::from([3; 32]), Account { nonce: 1, ..Default::default() }), // Override + (B256::from([4; 32]), Account::default()), + ], + destroyed_accounts: B256Set::from_iter([B256::from([6; 32])]), + }, + storages: B256Map::default(), + }; + + state1.extend_ref(&state2); + + // Check accounts are merged and sorted + assert_eq!(state1.accounts.accounts.len(), 4); + assert_eq!(state1.accounts.accounts[0].0, B256::from([1; 32])); + assert_eq!(state1.accounts.accounts[1].0, B256::from([2; 32])); + assert_eq!(state1.accounts.accounts[2].0, B256::from([3; 32])); + assert_eq!(state1.accounts.accounts[2].1.nonce, 1); // Should have state2's value + assert_eq!(state1.accounts.accounts[3].0, B256::from([4; 32])); + + // Check destroyed accounts are merged + assert!(state1.accounts.destroyed_accounts.contains(&B256::from([5; 32]))); + assert!(state1.accounts.destroyed_accounts.contains(&B256::from([6; 32]))); + } + + #[test] + fn test_hashed_storage_sorted_extend_ref() { + // Test normal extension + let mut storage1 = HashedStorageSorted { + non_zero_valued_slots: vec![ + (B256::from([1; 32]), U256::from(10)), + (B256::from([3; 32]), U256::from(30)), + ], + zero_valued_slots: B256Set::from_iter([B256::from([5; 32])]), + wiped: false, + }; + + let storage2 = HashedStorageSorted { + non_zero_valued_slots: vec![ + (B256::from([2; 32]), U256::from(20)), + (B256::from([3; 32]), U256::from(300)), // Override + (B256::from([4; 32]), U256::from(40)), + ], + zero_valued_slots: B256Set::from_iter([B256::from([6; 32])]), + wiped: false, + }; + + storage1.extend_ref(&storage2); + + assert_eq!(storage1.non_zero_valued_slots.len(), 4); + assert_eq!(storage1.non_zero_valued_slots[0].0, B256::from([1; 32])); + assert_eq!(storage1.non_zero_valued_slots[1].0, B256::from([2; 32])); + assert_eq!(storage1.non_zero_valued_slots[2].0, B256::from([3; 32])); + assert_eq!(storage1.non_zero_valued_slots[2].1, U256::from(300)); // Should have storage2's value + assert_eq!(storage1.non_zero_valued_slots[3].0, B256::from([4; 32])); + assert!(storage1.zero_valued_slots.contains(&B256::from([5; 32]))); + assert!(storage1.zero_valued_slots.contains(&B256::from([6; 32]))); + assert!(!storage1.wiped); + + // Test wiped storage + let mut storage3 = HashedStorageSorted { + non_zero_valued_slots: vec![(B256::from([1; 32]), U256::from(10))], + zero_valued_slots: B256Set::from_iter([B256::from([2; 32])]), + wiped: false, + }; + + let storage4 = HashedStorageSorted { + non_zero_valued_slots: vec![(B256::from([3; 32]), U256::from(30))], + zero_valued_slots: B256Set::from_iter([B256::from([4; 32])]), + wiped: true, + }; + + storage3.extend_ref(&storage4); + + assert!(storage3.wiped); + // When wiped, should only have storage4's values + assert_eq!(storage3.non_zero_valued_slots.len(), 1); + assert_eq!(storage3.non_zero_valued_slots[0].0, B256::from([3; 32])); + assert_eq!(storage3.zero_valued_slots.len(), 1); + assert!(storage3.zero_valued_slots.contains(&B256::from([4; 32]))); + } } diff --git a/crates/trie/common/src/input.rs b/crates/trie/common/src/input.rs index fff50fbb7b0..522cfa9ed41 100644 --- a/crates/trie/common/src/input.rs +++ b/crates/trie/common/src/input.rs @@ -34,7 +34,7 @@ impl TrieInput { /// Create new trie input from the provided blocks, from oldest to newest. See the documentation /// for [`Self::extend_with_blocks`] for details. pub fn from_blocks<'a>( - blocks: impl IntoIterator)>, + blocks: impl IntoIterator, ) -> Self { let mut input = Self::default(); input.extend_with_blocks(blocks); @@ -47,14 +47,10 @@ impl TrieInput { /// constructed from the state of this block and the state itself, **without** trie updates. pub fn extend_with_blocks<'a>( &mut self, - blocks: impl IntoIterator)>, + blocks: impl IntoIterator, ) { for (hashed_state, trie_updates) in blocks { - if let Some(nodes) = trie_updates.as_ref() { - self.append_cached_ref(nodes, hashed_state); - } else { - self.append_ref(hashed_state); - } + self.append_cached_ref(trie_updates, hashed_state); } } diff --git a/crates/trie/common/src/lib.rs b/crates/trie/common/src/lib.rs index 70616ba5eb8..e4292a52016 100644 --- a/crates/trie/common/src/lib.rs +++ b/crates/trie/common/src/lib.rs @@ -36,7 +36,7 @@ mod nibbles; pub use nibbles::{Nibbles, StoredNibbles, StoredNibblesSubKey}; mod storage; -pub use storage::StorageTrieEntry; +pub use storage::{StorageTrieEntry, TrieChangeSetsEntry}; mod subnode; pub use subnode::StoredSubNode; @@ -57,6 +57,9 @@ pub mod updates; pub mod added_removed_keys; +/// Utilities used by other modules in this crate. +mod utils; + /// Bincode-compatible serde implementations for trie types. /// /// `bincode` crate allows for more efficient serialization of trie types, because it allows diff --git a/crates/trie/common/src/storage.rs b/crates/trie/common/src/storage.rs index 187a097bfd4..557b9e4a606 100644 --- a/crates/trie/common/src/storage.rs +++ b/crates/trie/common/src/storage.rs @@ -1,6 +1,8 @@ -use super::{BranchNodeCompact, StoredNibblesSubKey}; +use super::{BranchNodeCompact, Nibbles, StoredNibblesSubKey}; /// Account storage trie node. +/// +/// `nibbles` is the subkey when used as a value in the `StorageTrie` table. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))] pub struct StorageTrieEntry { @@ -31,3 +33,173 @@ impl reth_codecs::Compact for StorageTrieEntry { (this, buf) } } + +/// Trie changeset entry representing the state of a trie node before a block. +/// +/// `nibbles` is the subkey when used as a value in the changeset tables. +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize, serde::Deserialize))] +pub struct TrieChangeSetsEntry { + /// The nibbles of the intermediate node + pub nibbles: StoredNibblesSubKey, + /// Node value prior to the block being processed, None indicating it didn't exist. + pub node: Option, +} + +#[cfg(any(test, feature = "reth-codec"))] +impl reth_codecs::Compact for TrieChangeSetsEntry { + fn to_compact(&self, buf: &mut B) -> usize + where + B: bytes::BufMut + AsMut<[u8]>, + { + let nibbles_len = self.nibbles.to_compact(buf); + let node_len = self.node.as_ref().map(|node| node.to_compact(buf)).unwrap_or(0); + nibbles_len + node_len + } + + fn from_compact(buf: &[u8], len: usize) -> (Self, &[u8]) { + if len == 0 { + // Return an empty entry without trying to parse anything + return ( + Self { nibbles: StoredNibblesSubKey::from(Nibbles::default()), node: None }, + buf, + ) + } + + let (nibbles, buf) = StoredNibblesSubKey::from_compact(buf, 65); + + if len <= 65 { + return (Self { nibbles, node: None }, buf) + } + + let (node, buf) = BranchNodeCompact::from_compact(buf, len - 65); + (Self { nibbles, node: Some(node) }, buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bytes::BytesMut; + use reth_codecs::Compact; + + #[test] + fn test_trie_changesets_entry_full_empty() { + // Test a fully empty entry (empty nibbles, None node) + let entry = TrieChangeSetsEntry { nibbles: StoredNibblesSubKey::from(vec![]), node: None }; + + let mut buf = BytesMut::new(); + let len = entry.to_compact(&mut buf); + + // Empty nibbles takes 65 bytes (64 for padding + 1 for length) + // None node adds 0 bytes + assert_eq!(len, 65); + assert_eq!(buf.len(), 65); + + // Deserialize and verify + let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len); + assert_eq!(decoded.nibbles.0.to_vec(), Vec::::new()); + assert_eq!(decoded.node, None); + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_trie_changesets_entry_none_node() { + // Test non-empty nibbles with None node + let nibbles_data = vec![0x01, 0x02, 0x03, 0x04]; + let entry = TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey::from(nibbles_data.clone()), + node: None, + }; + + let mut buf = BytesMut::new(); + let len = entry.to_compact(&mut buf); + + // Nibbles takes 65 bytes regardless of content + assert_eq!(len, 65); + + // Deserialize and verify + let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len); + assert_eq!(decoded.nibbles.0.to_vec(), nibbles_data); + assert_eq!(decoded.node, None); + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_trie_changesets_entry_empty_path_with_node() { + // Test empty path with Some node + // Using the same signature as in the codebase: (state_mask, hash_mask, tree_mask, hashes, + // value) + let test_node = BranchNodeCompact::new( + 0b1111_1111_1111_1111, // state_mask: all children present + 0b1111_1111_1111_1111, // hash_mask: all have hashes + 0b0000_0000_0000_0000, // tree_mask: no embedded trees + vec![], // hashes + None, // value + ); + + let entry = TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey::from(vec![]), + node: Some(test_node.clone()), + }; + + let mut buf = BytesMut::new(); + let len = entry.to_compact(&mut buf); + + // Calculate expected length + let mut temp_buf = BytesMut::new(); + let node_len = test_node.to_compact(&mut temp_buf); + assert_eq!(len, 65 + node_len); + + // Deserialize and verify + let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len); + assert_eq!(decoded.nibbles.0.to_vec(), Vec::::new()); + assert_eq!(decoded.node, Some(test_node)); + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_trie_changesets_entry_normal() { + // Test normal case: non-empty path with Some node + let nibbles_data = vec![0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]; + // Using the same signature as in the codebase + let test_node = BranchNodeCompact::new( + 0b0000_0000_1111_0000, // state_mask: some children present + 0b0000_0000_0011_0000, // hash_mask: some have hashes + 0b0000_0000_0000_0000, // tree_mask: no embedded trees + vec![], // hashes (empty for this test) + None, // value + ); + + let entry = TrieChangeSetsEntry { + nibbles: StoredNibblesSubKey::from(nibbles_data.clone()), + node: Some(test_node.clone()), + }; + + let mut buf = BytesMut::new(); + let len = entry.to_compact(&mut buf); + + // Verify serialization length + let mut temp_buf = BytesMut::new(); + let node_len = test_node.to_compact(&mut temp_buf); + assert_eq!(len, 65 + node_len); + + // Deserialize and verify + let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, len); + assert_eq!(decoded.nibbles.0.to_vec(), nibbles_data); + assert_eq!(decoded.node, Some(test_node)); + assert_eq!(remaining.len(), 0); + } + + #[test] + fn test_trie_changesets_entry_from_compact_zero_len() { + // Test from_compact with zero length + let buf = vec![0x01, 0x02, 0x03]; + let (decoded, remaining) = TrieChangeSetsEntry::from_compact(&buf, 0); + + // Should return empty nibbles and None node + assert_eq!(decoded.nibbles.0.to_vec(), Vec::::new()); + assert_eq!(decoded.node, None); + assert_eq!(remaining, &buf[..]); // Buffer should be unchanged + } +} diff --git a/crates/trie/common/src/updates.rs b/crates/trie/common/src/updates.rs index 441e407db16..00a160c4f9f 100644 --- a/crates/trie/common/src/updates.rs +++ b/crates/trie/common/src/updates.rs @@ -1,4 +1,4 @@ -use crate::{BranchNodeCompact, HashBuilder, Nibbles}; +use crate::{utils::extend_sorted_vec, BranchNodeCompact, HashBuilder, Nibbles}; use alloc::{ collections::{btree_map::BTreeMap, btree_set::BTreeSet}, vec::Vec, @@ -438,6 +438,11 @@ pub struct TrieUpdatesSorted { } impl TrieUpdatesSorted { + /// Returns `true` if the updates are empty. + pub fn is_empty(&self) -> bool { + self.account_nodes.is_empty() && self.storage_tries.is_empty() + } + /// Returns reference to updated account nodes. pub fn account_nodes_ref(&self) -> &[(Nibbles, Option)] { &self.account_nodes @@ -447,6 +452,24 @@ impl TrieUpdatesSorted { pub const fn storage_tries_ref(&self) -> &B256Map { &self.storage_tries } + + /// Extends the trie updates with another set of sorted updates. + /// + /// This merges the account nodes and storage tries from `other` into `self`. + /// Account nodes are merged and re-sorted, with `other`'s values taking precedence + /// for duplicate keys. + pub fn extend_ref(&mut self, other: &Self) { + // Extend account nodes + extend_sorted_vec(&mut self.account_nodes, &other.account_nodes); + + // Merge storage tries + for (hashed_address, storage_trie) in &other.storage_tries { + self.storage_tries + .entry(*hashed_address) + .and_modify(|existing| existing.extend_ref(storage_trie)) + .or_insert_with(|| storage_trie.clone()); + } + } } impl AsRef for TrieUpdatesSorted { @@ -455,6 +478,29 @@ impl AsRef for TrieUpdatesSorted { } } +impl From for TrieUpdates { + fn from(sorted: TrieUpdatesSorted) -> Self { + let mut account_nodes = HashMap::default(); + let mut removed_nodes = HashSet::default(); + + for (nibbles, node) in sorted.account_nodes { + if let Some(node) = node { + account_nodes.insert(nibbles, node); + } else { + removed_nodes.insert(nibbles); + } + } + + let storage_tries = sorted + .storage_tries + .into_iter() + .map(|(address, storage)| (address, storage.into())) + .collect(); + + Self { account_nodes, removed_nodes, storage_tries } + } +} + /// Sorted storage trie updates reference used for serializing to file. #[derive(PartialEq, Eq, Clone, Default, Debug)] #[cfg_attr(any(test, feature = "serde"), derive(serde::Serialize))] @@ -488,6 +534,23 @@ impl StorageTrieUpdatesSorted { pub fn storage_nodes_ref(&self) -> &[(Nibbles, Option)] { &self.storage_nodes } + + /// Extends the storage trie updates with another set of sorted updates. + /// + /// If `other` is marked as deleted, this will be marked as deleted and all nodes cleared. + /// Otherwise, nodes are merged with `other`'s values taking precedence for duplicates. + pub fn extend_ref(&mut self, other: &Self) { + if other.is_deleted { + self.is_deleted = true; + self.storage_nodes.clear(); + self.storage_nodes.extend(other.storage_nodes.iter().cloned()); + return; + } + + // Extend storage nodes + extend_sorted_vec(&mut self.storage_nodes, &other.storage_nodes); + self.is_deleted = self.is_deleted || other.is_deleted; + } } /// Excludes empty nibbles from the given iterator. @@ -502,6 +565,153 @@ fn exclude_empty_from_pair( iter.into_iter().filter(|(n, _)| !n.is_empty()) } +impl From for StorageTrieUpdates { + fn from(sorted: StorageTrieUpdatesSorted) -> Self { + let mut storage_nodes = HashMap::default(); + let mut removed_nodes = HashSet::default(); + + for (nibbles, node) in sorted.storage_nodes { + if let Some(node) = node { + storage_nodes.insert(nibbles, node); + } else { + removed_nodes.insert(nibbles); + } + } + + Self { is_deleted: sorted.is_deleted, storage_nodes, removed_nodes } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_primitives::B256; + + #[test] + fn test_trie_updates_sorted_extend_ref() { + // Test extending with empty updates + let mut updates1 = TrieUpdatesSorted::default(); + let updates2 = TrieUpdatesSorted::default(); + updates1.extend_ref(&updates2); + assert_eq!(updates1.account_nodes.len(), 0); + assert_eq!(updates1.storage_tries.len(), 0); + + // Test extending account nodes + let mut updates1 = TrieUpdatesSorted { + account_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x01]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x03]), None), + ], + storage_tries: B256Map::default(), + }; + let updates2 = TrieUpdatesSorted { + account_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x02]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x03]), Some(BranchNodeCompact::default())), /* Override */ + ], + storage_tries: B256Map::default(), + }; + updates1.extend_ref(&updates2); + assert_eq!(updates1.account_nodes.len(), 3); + // Should be sorted: 0x01, 0x02, 0x03 + assert_eq!(updates1.account_nodes[0].0, Nibbles::from_nibbles_unchecked([0x01])); + assert_eq!(updates1.account_nodes[1].0, Nibbles::from_nibbles_unchecked([0x02])); + assert_eq!(updates1.account_nodes[2].0, Nibbles::from_nibbles_unchecked([0x03])); + // 0x03 should have Some value from updates2 (override) + assert!(updates1.account_nodes[2].1.is_some()); + + // Test extending storage tries + let storage_trie1 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![( + Nibbles::from_nibbles_unchecked([0x0a]), + Some(BranchNodeCompact::default()), + )], + }; + let storage_trie2 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![(Nibbles::from_nibbles_unchecked([0x0b]), None)], + }; + + let hashed_address1 = B256::from([1; 32]); + let hashed_address2 = B256::from([2; 32]); + + let mut updates1 = TrieUpdatesSorted { + account_nodes: vec![], + storage_tries: B256Map::from_iter([(hashed_address1, storage_trie1.clone())]), + }; + let updates2 = TrieUpdatesSorted { + account_nodes: vec![], + storage_tries: B256Map::from_iter([ + (hashed_address1, storage_trie2), + (hashed_address2, storage_trie1), + ]), + }; + updates1.extend_ref(&updates2); + assert_eq!(updates1.storage_tries.len(), 2); + assert!(updates1.storage_tries.contains_key(&hashed_address1)); + assert!(updates1.storage_tries.contains_key(&hashed_address2)); + // Check that storage trie for hashed_address1 was extended + let merged_storage = &updates1.storage_tries[&hashed_address1]; + assert_eq!(merged_storage.storage_nodes.len(), 2); + } + + #[test] + fn test_storage_trie_updates_sorted_extend_ref_deleted() { + // Test case 1: Extending with a deleted storage trie that has nodes + let mut storage1 = StorageTrieUpdatesSorted { + is_deleted: false, + storage_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x01]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x02]), None), + ], + }; + + let storage2 = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x03]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x04]), None), + ], + }; + + storage1.extend_ref(&storage2); + + // Should be marked as deleted + assert!(storage1.is_deleted); + // Original nodes should be cleared, but other's nodes should be added + assert_eq!(storage1.storage_nodes.len(), 2); + assert_eq!(storage1.storage_nodes[0].0, Nibbles::from_nibbles_unchecked([0x03])); + assert_eq!(storage1.storage_nodes[1].0, Nibbles::from_nibbles_unchecked([0x04])); + + // Test case 2: Extending a deleted storage trie with more nodes + let mut storage3 = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![( + Nibbles::from_nibbles_unchecked([0x05]), + Some(BranchNodeCompact::default()), + )], + }; + + let storage4 = StorageTrieUpdatesSorted { + is_deleted: true, + storage_nodes: vec![ + (Nibbles::from_nibbles_unchecked([0x06]), Some(BranchNodeCompact::default())), + (Nibbles::from_nibbles_unchecked([0x07]), None), + ], + }; + + storage3.extend_ref(&storage4); + + // Should remain deleted + assert!(storage3.is_deleted); + // Should have nodes from other (original cleared then extended) + assert_eq!(storage3.storage_nodes.len(), 2); + assert_eq!(storage3.storage_nodes[0].0, Nibbles::from_nibbles_unchecked([0x06])); + assert_eq!(storage3.storage_nodes[1].0, Nibbles::from_nibbles_unchecked([0x07])); + } +} + /// Bincode-compatible trie updates type serde implementations. #[cfg(feature = "serde-bincode-compat")] pub mod serde_bincode_compat { @@ -717,7 +927,7 @@ pub mod serde_bincode_compat { } #[cfg(all(test, feature = "serde"))] -mod tests { +mod serde_tests { use super::*; #[test] diff --git a/crates/trie/common/src/utils.rs b/crates/trie/common/src/utils.rs new file mode 100644 index 00000000000..e5d16d3ef51 --- /dev/null +++ b/crates/trie/common/src/utils.rs @@ -0,0 +1,53 @@ +use alloc::vec::Vec; + +/// Helper function to extend a sorted vector with another sorted vector. +/// Values from `other` take precedence for duplicate keys. +/// +/// This function efficiently merges two sorted vectors by: +/// 1. Iterating through the target vector with mutable references +/// 2. Using a peekable iterator for the other vector +/// 3. For each target item, processing other items that come before or equal to it +/// 4. Collecting items from other that need to be inserted +/// 5. Appending and re-sorting only if new items were added +pub(crate) fn extend_sorted_vec(target: &mut Vec<(K, V)>, other: &[(K, V)]) +where + K: Clone + Ord + core::hash::Hash + Eq, + V: Clone, +{ + if other.is_empty() { + return; + } + + let mut other_iter = other.iter().peekable(); + let mut to_insert = Vec::new(); + + // Iterate through target and update/collect items from other + for target_item in target.iter_mut() { + while let Some(other_item) = other_iter.peek() { + use core::cmp::Ordering; + match other_item.0.cmp(&target_item.0) { + Ordering::Less => { + // Other item comes before current target item, collect it + to_insert.push(other_iter.next().unwrap().clone()); + } + Ordering::Equal => { + // Same key, update target with other's value + target_item.1 = other_iter.next().unwrap().1.clone(); + break; + } + Ordering::Greater => { + // Other item comes after current target item, keep target unchanged + break; + } + } + } + } + + // Append collected new items, as well as any remaining from `other` which are necessarily also + // new, and sort if needed + if !to_insert.is_empty() || other_iter.peek().is_some() { + target.extend(to_insert); + target.extend(other_iter.cloned()); + target.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + } +} diff --git a/crates/trie/db/src/trie_cursor.rs b/crates/trie/db/src/trie_cursor.rs index 62d376d1b54..b1e9032fc0f 100644 --- a/crates/trie/db/src/trie_cursor.rs +++ b/crates/trie/db/src/trie_cursor.rs @@ -7,7 +7,7 @@ use reth_db_api::{ }; use reth_trie::{ trie_cursor::{TrieCursor, TrieCursorFactory}, - updates::StorageTrieUpdates, + updates::StorageTrieUpdatesSorted, BranchNodeCompact, Nibbles, StorageTrieEntry, StoredNibbles, StoredNibblesSubKey, }; @@ -110,31 +110,19 @@ where + DbDupCursorRO + DbDupCursorRW, { - /// Writes storage updates - pub fn write_storage_trie_updates( + /// Writes storage updates that are already sorted + pub fn write_storage_trie_updates_sorted( &mut self, - updates: &StorageTrieUpdates, + updates: &StorageTrieUpdatesSorted, ) -> Result { // The storage trie for this account has to be deleted. if updates.is_deleted() && self.cursor.seek_exact(self.hashed_address)?.is_some() { self.cursor.delete_current_duplicates()?; } - // Merge updated and removed nodes. Updated nodes must take precedence. - let mut storage_updates = updates - .removed_nodes_ref() - .iter() - .filter_map(|n| (!updates.storage_nodes_ref().contains_key(n)).then_some((n, None))) - .collect::>(); - storage_updates.extend( - updates.storage_nodes_ref().iter().map(|(nibbles, node)| (nibbles, Some(node))), - ); - - // Sort trie node updates. - storage_updates.sort_unstable_by(|a, b| a.0.cmp(b.0)); - let mut num_entries = 0; - for (nibbles, maybe_updated) in storage_updates.into_iter().filter(|(n, _)| !n.is_empty()) { + for (nibbles, maybe_updated) in updates.storage_nodes.iter().filter(|(n, _)| !n.is_empty()) + { num_entries += 1; let nibbles = StoredNibblesSubKey(*nibbles); // Delete the old entry if it exists. diff --git a/crates/trie/db/tests/trie.rs b/crates/trie/db/tests/trie.rs index e9fcb5a1c48..8f543a711d8 100644 --- a/crates/trie/db/tests/trie.rs +++ b/crates/trie/db/tests/trie.rs @@ -81,7 +81,11 @@ fn incremental_vs_full_root(inputs: &[&str], modified: &str) { let modified_root = loader.root().unwrap(); // Update the intermediate roots table so that we can run the incremental verification - tx.write_storage_trie_updates(core::iter::once((&hashed_address, &trie_updates))).unwrap(); + tx.write_storage_trie_updates_sorted(core::iter::once(( + &hashed_address, + &trie_updates.into_sorted(), + ))) + .unwrap(); // 3. Calculate the incremental root let mut storage_changes = PrefixSetMut::default(); @@ -620,7 +624,7 @@ fn account_trie_around_extension_node_with_dbtrie() { let (got, updates) = StateRoot::from_tx(tx.tx_ref()).root_with_updates().unwrap(); assert_eq!(expected, got); - tx.write_trie_updates(&updates).unwrap(); + tx.write_trie_updates(updates).unwrap(); // read the account updates from the db let mut accounts_trie = tx.tx_ref().cursor_read::().unwrap(); @@ -667,7 +671,7 @@ proptest! { state.iter().map(|(&key, &balance)| (key, (Account { balance, ..Default::default() }, std::iter::empty()))) ); assert_eq!(expected_root, state_root); - tx.write_trie_updates(&trie_updates).unwrap(); + tx.write_trie_updates(trie_updates).unwrap(); } } } diff --git a/crates/trie/parallel/benches/root.rs b/crates/trie/parallel/benches/root.rs index fe1953b9055..48657cc8a70 100644 --- a/crates/trie/parallel/benches/root.rs +++ b/crates/trie/parallel/benches/root.rs @@ -33,7 +33,7 @@ pub fn calculate_state_root(c: &mut Criterion) { provider_rw.write_hashed_state(&db_state.into_sorted()).unwrap(); let (_, updates) = StateRoot::from_tx(provider_rw.tx_ref()).root_with_updates().unwrap(); - provider_rw.write_trie_updates(&updates).unwrap(); + provider_rw.write_trie_updates(updates).unwrap(); provider_rw.commit().unwrap(); } diff --git a/crates/trie/sparse-parallel/src/trie.rs b/crates/trie/sparse-parallel/src/trie.rs index 50c9a79bd05..472624f99d7 100644 --- a/crates/trie/sparse-parallel/src/trie.rs +++ b/crates/trie/sparse-parallel/src/trie.rs @@ -4998,9 +4998,12 @@ mod tests { state.keys().copied(), ); + // Extract account nodes before moving hash_builder_updates + let hash_builder_account_nodes = hash_builder_updates.account_nodes.clone(); + // Write trie updates to the database let provider_rw = provider_factory.provider_rw().unwrap(); - provider_rw.write_trie_updates(&hash_builder_updates).unwrap(); + provider_rw.write_trie_updates(hash_builder_updates).unwrap(); provider_rw.commit().unwrap(); // Assert that the sparse trie root matches the hash builder root @@ -5008,7 +5011,7 @@ mod tests { // Assert that the sparse trie updates match the hash builder updates pretty_assertions::assert_eq!( BTreeMap::from_iter(sparse_updates.updated_nodes), - BTreeMap::from_iter(hash_builder_updates.account_nodes) + BTreeMap::from_iter(hash_builder_account_nodes) ); // Assert that the sparse trie nodes match the hash builder proof nodes assert_eq_parallel_sparse_trie_proof_nodes( @@ -5043,9 +5046,12 @@ mod tests { state.keys().copied(), ); + // Extract account nodes before moving hash_builder_updates + let hash_builder_account_nodes = hash_builder_updates.account_nodes.clone(); + // Write trie updates to the database let provider_rw = provider_factory.provider_rw().unwrap(); - provider_rw.write_trie_updates(&hash_builder_updates).unwrap(); + provider_rw.write_trie_updates(hash_builder_updates).unwrap(); provider_rw.commit().unwrap(); // Assert that the sparse trie root matches the hash builder root @@ -5053,7 +5059,7 @@ mod tests { // Assert that the sparse trie updates match the hash builder updates pretty_assertions::assert_eq!( BTreeMap::from_iter(sparse_updates.updated_nodes), - BTreeMap::from_iter(hash_builder_updates.account_nodes) + BTreeMap::from_iter(hash_builder_account_nodes) ); // Assert that the sparse trie nodes match the hash builder proof nodes assert_eq_parallel_sparse_trie_proof_nodes( diff --git a/crates/trie/sparse/src/trie.rs b/crates/trie/sparse/src/trie.rs index 89a23851e28..cbffe5e7563 100644 --- a/crates/trie/sparse/src/trie.rs +++ b/crates/trie/sparse/src/trie.rs @@ -3034,9 +3034,12 @@ mod tests { state.keys().copied(), ); + // Extract account nodes before moving hash_builder_updates + let hash_builder_account_nodes = hash_builder_updates.account_nodes.clone(); + // Write trie updates to the database let provider_rw = provider_factory.provider_rw().unwrap(); - provider_rw.write_trie_updates(&hash_builder_updates).unwrap(); + provider_rw.write_trie_updates(hash_builder_updates).unwrap(); provider_rw.commit().unwrap(); // Assert that the sparse trie root matches the hash builder root @@ -3044,7 +3047,7 @@ mod tests { // Assert that the sparse trie updates match the hash builder updates pretty_assertions::assert_eq!( BTreeMap::from_iter(sparse_updates.updated_nodes), - BTreeMap::from_iter(hash_builder_updates.account_nodes) + BTreeMap::from_iter(hash_builder_account_nodes) ); // Assert that the sparse trie nodes match the hash builder proof nodes assert_eq_sparse_trie_proof_nodes(&updated_sparse, hash_builder_proof_nodes); @@ -3076,9 +3079,12 @@ mod tests { state.keys().copied(), ); + // Extract account nodes before moving hash_builder_updates + let hash_builder_account_nodes = hash_builder_updates.account_nodes.clone(); + // Write trie updates to the database let provider_rw = provider_factory.provider_rw().unwrap(); - provider_rw.write_trie_updates(&hash_builder_updates).unwrap(); + provider_rw.write_trie_updates(hash_builder_updates).unwrap(); provider_rw.commit().unwrap(); // Assert that the sparse trie root matches the hash builder root @@ -3086,7 +3092,7 @@ mod tests { // Assert that the sparse trie updates match the hash builder updates pretty_assertions::assert_eq!( BTreeMap::from_iter(sparse_updates.updated_nodes), - BTreeMap::from_iter(hash_builder_updates.account_nodes) + BTreeMap::from_iter(hash_builder_account_nodes) ); // Assert that the sparse trie nodes match the hash builder proof nodes assert_eq_sparse_trie_proof_nodes(&updated_sparse, hash_builder_proof_nodes); diff --git a/crates/trie/trie/src/trie_cursor/mod.rs b/crates/trie/trie/src/trie_cursor/mod.rs index 01eea4c40e6..269611150d6 100644 --- a/crates/trie/trie/src/trie_cursor/mod.rs +++ b/crates/trie/trie/src/trie_cursor/mod.rs @@ -58,3 +58,48 @@ pub trait TrieCursor: Send + Sync { /// Get the current entry. fn current(&mut self) -> Result, DatabaseError>; } + +/// Iterator wrapper for `TrieCursor` types +#[derive(Debug)] +pub struct TrieCursorIter<'a, C> { + cursor: &'a mut C, + /// The initial value from seek, if any + initial: Option>, +} + +impl<'a, C> TrieCursorIter<'a, C> { + /// Create a new iterator from a mutable reference to a cursor. The Iterator will start from the + /// empty path. + pub fn new(cursor: &'a mut C) -> Self + where + C: TrieCursor, + { + let initial = cursor.seek(Nibbles::default()).transpose(); + Self { cursor, initial } + } +} + +impl<'a, C> From<&'a mut C> for TrieCursorIter<'a, C> +where + C: TrieCursor, +{ + fn from(cursor: &'a mut C) -> Self { + Self::new(cursor) + } +} + +impl<'a, C> Iterator for TrieCursorIter<'a, C> +where + C: TrieCursor, +{ + type Item = Result<(Nibbles, BranchNodeCompact), DatabaseError>; + + fn next(&mut self) -> Option { + // If we have an initial value from seek, return it first + if let Some(initial) = self.initial.take() { + return Some(initial); + } + + self.cursor.next().transpose() + } +} diff --git a/docs/vocs/docs/pages/cli/reth/stage/drop.mdx b/docs/vocs/docs/pages/cli/reth/stage/drop.mdx index e2ba5751b52..79ee52accd9 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/drop.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/drop.mdx @@ -72,17 +72,18 @@ Database: Possible values: - - headers: The headers stage within the pipeline - - bodies: The bodies stage within the pipeline - - senders: The senders stage within the pipeline - - execution: The execution stage within the pipeline - - account-hashing: The account hashing stage within the pipeline - - storage-hashing: The storage hashing stage within the pipeline - - hashing: The account and storage hashing stages within the pipeline - - merkle: The merkle stage within the pipeline - - tx-lookup: The transaction lookup stage within the pipeline - - account-history: The account history stage within the pipeline - - storage-history: The storage history stage within the pipeline + - headers: The headers stage within the pipeline + - bodies: The bodies stage within the pipeline + - senders: The senders stage within the pipeline + - execution: The execution stage within the pipeline + - account-hashing: The account hashing stage within the pipeline + - storage-hashing: The storage hashing stage within the pipeline + - hashing: The account and storage hashing stages within the pipeline + - merkle: The merkle stage within the pipeline + - merkle-changesets: The merkle changesets stage within the pipeline + - tx-lookup: The transaction lookup stage within the pipeline + - account-history: The account history stage within the pipeline + - storage-history: The storage history stage within the pipeline Logging: --log.stdout.format diff --git a/docs/vocs/docs/pages/cli/reth/stage/run.mdx b/docs/vocs/docs/pages/cli/reth/stage/run.mdx index 2af69a053d6..d2fe67687e8 100644 --- a/docs/vocs/docs/pages/cli/reth/stage/run.mdx +++ b/docs/vocs/docs/pages/cli/reth/stage/run.mdx @@ -101,17 +101,18 @@ Database: The name of the stage to run Possible values: - - headers: The headers stage within the pipeline - - bodies: The bodies stage within the pipeline - - senders: The senders stage within the pipeline - - execution: The execution stage within the pipeline - - account-hashing: The account hashing stage within the pipeline - - storage-hashing: The storage hashing stage within the pipeline - - hashing: The account and storage hashing stages within the pipeline - - merkle: The merkle stage within the pipeline - - tx-lookup: The transaction lookup stage within the pipeline - - account-history: The account history stage within the pipeline - - storage-history: The storage history stage within the pipeline + - headers: The headers stage within the pipeline + - bodies: The bodies stage within the pipeline + - senders: The senders stage within the pipeline + - execution: The execution stage within the pipeline + - account-hashing: The account hashing stage within the pipeline + - storage-hashing: The storage hashing stage within the pipeline + - hashing: The account and storage hashing stages within the pipeline + - merkle: The merkle stage within the pipeline + - merkle-changesets: The merkle changesets stage within the pipeline + - tx-lookup: The transaction lookup stage within the pipeline + - account-history: The account history stage within the pipeline + - storage-history: The storage history stage within the pipeline Networking: -d, --disable-discovery diff --git a/examples/custom-node/src/engine.rs b/examples/custom-node/src/engine.rs index 357290e14d7..0c80e52a661 100644 --- a/examples/custom-node/src/engine.rs +++ b/examples/custom-node/src/engine.rs @@ -6,7 +6,7 @@ use crate::{ }; use alloy_eips::eip2718::WithEncoded; use op_alloy_rpc_types_engine::{OpExecutionData, OpExecutionPayload}; -use reth_chain_state::ExecutedBlockWithTrieUpdates; +use reth_chain_state::ExecutedBlock; use reth_engine_primitives::EngineApiValidator; use reth_ethereum::{ node::api::{ @@ -167,7 +167,7 @@ impl BuiltPayload for CustomBuiltPayload { self.0.fees() } - fn executed_block(&self) -> Option> { + fn executed_block(&self) -> Option> { self.0.executed_block() }