From 121160d248a92cddadfd8e27d4073e2ff9e332dc Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 12 Feb 2026 13:02:02 -0500 Subject: [PATCH] refactor(db): use hashed state as canonical state representation (#21115) Co-authored-by: Amp Co-authored-by: Dan Cline <6798349+Rjected@users.noreply.github.com> Co-authored-by: joshieDo <93316087+joshieDo@users.noreply.github.com> --- crates/chain-state/src/in_memory.rs | 8 + crates/chain-state/src/memory_overlay.rs | 20 + crates/cli/commands/src/db/account_storage.rs | 111 +- crates/cli/commands/src/db/get.rs | 12 +- crates/cli/commands/src/db/state.rs | 86 +- crates/cli/commands/src/node.rs | 4 +- crates/cli/commands/src/stage/drop.rs | 11 +- crates/e2e-test-utils/src/testsuite/setup.rs | 30 +- crates/engine/service/src/service.rs | 4 +- crates/engine/tree/Cargo.toml | 7 + crates/engine/tree/src/tree/cached_state.rs | 8 + .../tree/src/tree/instrumented_state.rs | 11 + crates/engine/tree/src/tree/mod.rs | 20 +- .../src/tree/payload_processor/multiproof.rs | 7 +- .../engine/tree/src/tree/payload_validator.rs | 8 +- crates/engine/tree/src/tree/tests.rs | 1 + .../engine/tree/tests/e2e-testsuite/main.rs | 165 +- crates/exex/exex/src/backfill/test_utils.rs | 11 +- crates/node/core/src/args/mod.rs | 2 +- crates/node/core/src/args/storage.rs | 17 +- crates/node/core/src/node_config.rs | 15 +- crates/primitives-traits/src/lib.rs | 2 +- crates/primitives-traits/src/storage.rs | 121 +- .../src/segments/user/storage_history.rs | 4 +- crates/revm/src/test_utils.rs | 8 + crates/rpc/rpc-eth-types/src/cache/db.rs | 8 + crates/stages/stages/src/stages/execution.rs | 10 +- .../stages/src/stages/hashing_account.rs | 26 +- .../stages/src/stages/hashing_storage.rs | 18 +- crates/stages/stages/src/stages/merkle.rs | 3 +- crates/stages/stages/src/stages/utils.rs | 5 +- crates/stages/stages/tests/pipeline.rs | 163 +- crates/storage/db-api/src/models/metadata.rs | 18 +- .../storage/provider/src/changeset_walker.rs | 7 +- .../src/providers/blockchain_provider.rs | 12 +- .../provider/src/providers/consistent.rs | 706 +++++++- .../src/providers/database/provider.rs | 1524 +++++++++++++++-- .../src/providers/rocksdb/invariants.rs | 5 +- .../src/providers/rocksdb/provider.rs | 4 +- .../src/providers/state/historical.rs | 501 +++++- .../provider/src/providers/state/latest.rs | 234 ++- .../provider/src/providers/state/overlay.rs | 19 +- .../src/providers/static_file/manager.rs | 22 +- .../provider/src/providers/static_file/mod.rs | 14 +- .../storage/provider/src/test_utils/mock.rs | 32 +- crates/storage/provider/src/traits/full.rs | 8 +- crates/storage/rpc-provider/src/lib.rs | 8 + crates/storage/storage-api/src/hashing.rs | 3 +- crates/storage/storage-api/src/history.rs | 4 +- crates/storage/storage-api/src/macros.rs | 1 + crates/storage/storage-api/src/noop.rs | 29 +- crates/storage/storage-api/src/state.rs | 15 + crates/storage/storage-api/src/storage.rs | 48 +- crates/trie/common/src/key.rs | 17 + crates/trie/db/src/changesets.rs | 26 +- crates/trie/db/src/lib.rs | 2 +- crates/trie/db/src/prefix_set.rs | 20 +- crates/trie/db/src/state.rs | 320 +++- crates/trie/db/src/storage.rs | 130 +- .../cli/reth/db/checksum/use_hashed_state.mdx | 170 ++ 60 files changed, 4341 insertions(+), 484 deletions(-) create mode 100644 docs/vocs/docs/pages/cli/reth/db/checksum/use_hashed_state.mdx diff --git a/crates/chain-state/src/in_memory.rs b/crates/chain-state/src/in_memory.rs index 00598e413b..4eac2cb580 100644 --- a/crates/chain-state/src/in_memory.rs +++ b/crates/chain-state/src/in_memory.rs @@ -1061,6 +1061,14 @@ mod tests { ) -> ProviderResult> { Ok(None) } + + fn storage_by_hashed_key( + &self, + _address: Address, + _hashed_storage_key: StorageKey, + ) -> ProviderResult> { + Ok(None) + } } impl BytecodeReader for MockStateProvider { diff --git a/crates/chain-state/src/memory_overlay.rs b/crates/chain-state/src/memory_overlay.rs index a4269886da..f70fcbb89b 100644 --- a/crates/chain-state/src/memory_overlay.rs +++ b/crates/chain-state/src/memory_overlay.rs @@ -223,6 +223,26 @@ impl StateProvider for MemoryOverlayStateProviderRef<'_, N> { self.historical.storage(address, storage_key) } + + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: StorageKey, + ) -> ProviderResult> { + let hashed_address = keccak256(address); + let state = &self.trie_input().state; + + if let Some(hs) = state.storages.get(&hashed_address) { + if let Some(value) = hs.storage.get(&hashed_storage_key) { + return Ok(Some(*value)); + } + if hs.wiped { + return Ok(Some(StorageValue::ZERO)); + } + } + + self.historical.storage_by_hashed_key(address, hashed_storage_key) + } } impl BytecodeReader for MemoryOverlayStateProviderRef<'_, N> { diff --git a/crates/cli/commands/src/db/account_storage.rs b/crates/cli/commands/src/db/account_storage.rs index f01fcce9c0..dc262eae59 100644 --- a/crates/cli/commands/src/db/account_storage.rs +++ b/crates/cli/commands/src/db/account_storage.rs @@ -5,6 +5,7 @@ use reth_codecs::Compact; use reth_db_api::{cursor::DbDupCursorRO, database::Database, tables, transaction::DbTx}; use reth_db_common::DbTool; use reth_node_builder::NodeTypesWithDB; +use reth_storage_api::StorageSettingsCache; use std::time::{Duration, Instant}; use tracing::info; @@ -22,52 +23,94 @@ impl Command { /// Execute `db account-storage` command pub fn execute(self, tool: &DbTool) -> eyre::Result<()> { let address = self.address; - let (slot_count, plain_size) = tool.provider_factory.db_ref().view(|tx| { - let mut cursor = tx.cursor_dup_read::()?; - let mut count = 0usize; - let mut total_value_bytes = 0usize; - let mut last_log = Instant::now(); + let use_hashed_state = tool.provider_factory.cached_storage_settings().use_hashed_state; - // Walk all storage entries for this address - let walker = cursor.walk_dup(Some(address), None)?; - for entry in walker { - let (_, storage_entry) = entry?; - count += 1; - // StorageEntry encodes as: 32 bytes (key/subkey uncompressed) + compressed U256 - let mut buf = Vec::new(); - let entry_len = storage_entry.to_compact(&mut buf); - total_value_bytes += entry_len; + let (slot_count, storage_size) = if use_hashed_state { + let hashed_address = keccak256(address); + tool.provider_factory.db_ref().view(|tx| { + let mut cursor = tx.cursor_dup_read::()?; + let mut count = 0usize; + let mut total_value_bytes = 0usize; + let mut last_log = Instant::now(); - if last_log.elapsed() >= LOG_INTERVAL { - info!( - target: "reth::cli", - address = %address, - slots = count, - key = %storage_entry.key, - "Processing storage slots" - ); - last_log = Instant::now(); + let walker = cursor.walk_dup(Some(hashed_address), None)?; + for entry in walker { + let (_, storage_entry) = entry?; + count += 1; + let mut buf = Vec::new(); + let entry_len = storage_entry.to_compact(&mut buf); + total_value_bytes += entry_len; + + if last_log.elapsed() >= LOG_INTERVAL { + info!( + target: "reth::cli", + address = %address, + slots = count, + key = %storage_entry.key, + "Processing hashed storage slots" + ); + last_log = Instant::now(); + } } - } - // Add 20 bytes for the Address key (stored once per account in dupsort) - let total_size = if count > 0 { 20 + total_value_bytes } else { 0 }; + let total_size = if count > 0 { 32 + total_value_bytes } else { 0 }; - Ok::<_, eyre::Report>((count, total_size)) - })??; + Ok::<_, eyre::Report>((count, total_size)) + })?? + } else { + tool.provider_factory.db_ref().view(|tx| { + let mut cursor = tx.cursor_dup_read::()?; + let mut count = 0usize; + let mut total_value_bytes = 0usize; + let mut last_log = Instant::now(); - // Estimate hashed storage size: 32-byte B256 key instead of 20-byte Address - let hashed_size_estimate = if slot_count > 0 { plain_size + 12 } else { 0 }; - let total_estimate = plain_size + hashed_size_estimate; + // Walk all storage entries for this address + let walker = cursor.walk_dup(Some(address), None)?; + for entry in walker { + let (_, storage_entry) = entry?; + count += 1; + let mut buf = Vec::new(); + // StorageEntry encodes as: 32 bytes (key/subkey uncompressed) + compressed U256 + let entry_len = storage_entry.to_compact(&mut buf); + total_value_bytes += entry_len; + + if last_log.elapsed() >= LOG_INTERVAL { + info!( + target: "reth::cli", + address = %address, + slots = count, + key = %storage_entry.key, + "Processing storage slots" + ); + last_log = Instant::now(); + } + } + + // Add 20 bytes for the Address key (stored once per account in dupsort) + let total_size = if count > 0 { 20 + total_value_bytes } else { 0 }; + + Ok::<_, eyre::Report>((count, total_size)) + })?? + }; let hashed_address = keccak256(address); println!("Account: {address}"); println!("Hashed address: {hashed_address}"); println!("Storage slots: {slot_count}"); - println!("Plain storage size: {} (estimated)", human_bytes(plain_size as f64)); - println!("Hashed storage size: {} (estimated)", human_bytes(hashed_size_estimate as f64)); - println!("Total estimated size: {}", human_bytes(total_estimate as f64)); + if use_hashed_state { + println!("Hashed storage size: {} (estimated)", human_bytes(storage_size as f64)); + } else { + // Estimate hashed storage size: 32-byte B256 key instead of 20-byte Address + let hashed_size_estimate = if slot_count > 0 { storage_size + 12 } else { 0 }; + let total_estimate = storage_size + hashed_size_estimate; + println!("Plain storage size: {} (estimated)", human_bytes(storage_size as f64)); + println!( + "Hashed storage size: {} (estimated)", + human_bytes(hashed_size_estimate as f64) + ); + println!("Total estimated size: {}", human_bytes(total_estimate as f64)); + } Ok(()) } diff --git a/crates/cli/commands/src/db/get.rs b/crates/cli/commands/src/db/get.rs index 1b3739bb46..05f424c2e4 100644 --- a/crates/cli/commands/src/db/get.rs +++ b/crates/cli/commands/src/db/get.rs @@ -98,7 +98,8 @@ impl Command { )?; if let Some(entry) = entry { - println!("{}", serde_json::to_string_pretty(&entry)?); + let se: reth_primitives_traits::StorageEntry = entry.into(); + println!("{}", serde_json::to_string_pretty(&se)?); } else { error!(target: "reth::cli", "No content for the given table key."); } @@ -106,7 +107,14 @@ impl Command { } let changesets = provider.storage_changeset(key.block_number())?; - println!("{}", serde_json::to_string_pretty(&changesets)?); + let serializable: Vec<_> = changesets + .into_iter() + .map(|(addr, entry)| { + let se: reth_primitives_traits::StorageEntry = entry.into(); + (addr, se) + }) + .collect(); + println!("{}", serde_json::to_string_pretty(&serializable)?); return Ok(()); } diff --git a/crates/cli/commands/src/db/state.rs b/crates/cli/commands/src/db/state.rs index b28e4c2647..5b3d88e43f 100644 --- a/crates/cli/commands/src/db/state.rs +++ b/crates/cli/commands/src/db/state.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{Address, BlockNumber, B256, U256}; +use alloy_primitives::{keccak256, Address, BlockNumber, B256, U256}; use clap::Parser; use parking_lot::Mutex; use reth_db_api::{ @@ -63,39 +63,65 @@ impl Command { address: Address, limit: usize, ) -> eyre::Result<()> { + let use_hashed_state = tool.provider_factory.cached_storage_settings().use_hashed_state; + let entries = tool.provider_factory.db_ref().view(|tx| { - // Get account info - let account = tx.get::(address)?; - - // Get storage entries - let mut cursor = tx.cursor_dup_read::()?; - let mut entries = Vec::new(); - let mut last_log = Instant::now(); - - let walker = cursor.walk_dup(Some(address), None)?; - for (idx, entry) in walker.enumerate() { - let (_, storage_entry) = entry?; - - if storage_entry.value != U256::ZERO { - entries.push((storage_entry.key, storage_entry.value)); + let (account, walker_entries) = if use_hashed_state { + let hashed_address = keccak256(address); + let account = tx.get::(hashed_address)?; + let mut cursor = tx.cursor_dup_read::()?; + let walker = cursor.walk_dup(Some(hashed_address), None)?; + let mut entries = Vec::new(); + let mut last_log = Instant::now(); + for (idx, entry) in walker.enumerate() { + let (_, storage_entry) = entry?; + if storage_entry.value != U256::ZERO { + entries.push((storage_entry.key, storage_entry.value)); + } + if entries.len() >= limit { + break; + } + if last_log.elapsed() >= LOG_INTERVAL { + info!( + target: "reth::cli", + address = %address, + slots_scanned = idx, + "Scanning storage slots" + ); + last_log = Instant::now(); + } } - - if entries.len() >= limit { - break; + (account, entries) + } else { + // Get account info + let account = tx.get::(address)?; + // Get storage entries + let mut cursor = tx.cursor_dup_read::()?; + let walker = cursor.walk_dup(Some(address), None)?; + let mut entries = Vec::new(); + let mut last_log = Instant::now(); + for (idx, entry) in walker.enumerate() { + let (_, storage_entry) = entry?; + if storage_entry.value != U256::ZERO { + entries.push((storage_entry.key, storage_entry.value)); + } + if entries.len() >= limit { + break; + } + if last_log.elapsed() >= LOG_INTERVAL { + info!( + target: "reth::cli", + address = %address, + slots_scanned = idx, + "Scanning storage slots" + ); + last_log = Instant::now(); + } } + (account, entries) + }; - if last_log.elapsed() >= LOG_INTERVAL { - info!( - target: "reth::cli", - address = %address, - slots_scanned = idx, - "Scanning storage slots" - ); - last_log = Instant::now(); - } - } - - Ok::<_, eyre::Report>((account, entries)) + Ok::<_, eyre::Report>((account, walker_entries)) })??; let (account, storage_entries) = entries; diff --git a/crates/cli/commands/src/node.rs b/crates/cli/commands/src/node.rs index 98aade3280..176e2a86ff 100644 --- a/crates/cli/commands/src/node.rs +++ b/crates/cli/commands/src/node.rs @@ -119,8 +119,8 @@ pub struct NodeCommand Command { reset_stage_checkpoint(tx, StageId::SenderRecovery)?; } StageEnum::Execution => { - tx.clear::()?; - tx.clear::()?; + if provider_rw.cached_storage_settings().use_hashed_state { + tx.clear::()?; + tx.clear::()?; + reset_stage_checkpoint(tx, StageId::AccountHashing)?; + reset_stage_checkpoint(tx, StageId::StorageHashing)?; + } else { + tx.clear::()?; + tx.clear::()?; + } tx.clear::()?; tx.clear::()?; tx.clear::()?; diff --git a/crates/e2e-test-utils/src/testsuite/setup.rs b/crates/e2e-test-utils/src/testsuite/setup.rs index 4577a5ab0b..7bc98268d6 100644 --- a/crates/e2e-test-utils/src/testsuite/setup.rs +++ b/crates/e2e-test-utils/src/testsuite/setup.rs @@ -1,6 +1,6 @@ //! Test setup utilities for configuring the initial state. -use crate::{setup_engine_with_connection, testsuite::Environment, NodeBuilderHelper}; +use crate::{testsuite::Environment, E2ETestSetupBuilder, NodeBuilderHelper}; use alloy_eips::BlockNumberOrTag; use alloy_primitives::B256; use alloy_rpc_types_engine::{ForkchoiceState, PayloadAttributes}; @@ -38,6 +38,8 @@ pub struct Setup { shutdown_tx: Option>, /// Is this setup in dev mode pub is_dev: bool, + /// Whether to use v2 storage mode (hashed keys, static file changesets, rocksdb history) + pub storage_v2: bool, /// Tracks instance generic. _phantom: PhantomData, /// Holds the import result to keep nodes alive when using imported chain @@ -58,6 +60,7 @@ impl Default for Setup { tree_config: TreeConfig::default(), shutdown_tx: None, is_dev: true, + storage_v2: false, _phantom: Default::default(), import_result_holder: None, import_rlp_path: None, @@ -126,6 +129,12 @@ where self } + /// Enable v2 storage mode (hashed keys, static file changesets, rocksdb history) + pub const fn with_storage_v2(mut self) -> Self { + self.storage_v2 = true; + self + } + /// Apply setup using pre-imported chain data from RLP file pub async fn apply_with_import( &mut self, @@ -194,19 +203,28 @@ where self.shutdown_tx = Some(shutdown_tx); let is_dev = self.is_dev; + let storage_v2 = self.storage_v2; let node_count = self.network.node_count; + let tree_config = self.tree_config.clone(); let attributes_generator = Self::create_static_attributes_generator::(); - let result = setup_engine_with_connection::( + let mut builder = E2ETestSetupBuilder::::new( node_count, Arc::::new((*chain_spec).clone().into()), - is_dev, - self.tree_config.clone(), attributes_generator, - self.network.connect_nodes, ) - .await; + .with_tree_config_modifier(move |base| { + tree_config.clone().with_cross_block_cache_size(base.cross_block_cache_size()) + }) + .with_node_config_modifier(move |config| config.set_dev(is_dev)) + .with_connect_nodes(self.network.connect_nodes); + + if storage_v2 { + builder = builder.with_storage_v2(); + } + + let result = builder.build().await; let mut node_clients = Vec::new(); match result { diff --git a/crates/engine/service/src/service.rs b/crates/engine/service/src/service.rs index ab1f9ed306..af1e7c9880 100644 --- a/crates/engine/service/src/service.rs +++ b/crates/engine/service/src/service.rs @@ -20,7 +20,7 @@ use reth_node_types::{BlockTy, NodeTypes}; use reth_payload_builder::PayloadBuilderHandle; use reth_provider::{ providers::{BlockchainProvider, ProviderNodeTypes}, - ProviderFactory, + ProviderFactory, StorageSettingsCache, }; use reth_prune::PrunerWithFactory; use reth_stages_api::{MetricEventsSender, Pipeline}; @@ -94,6 +94,7 @@ where if chain_spec.is_optimism() { EngineApiKind::OpStack } else { EngineApiKind::Ethereum }; let downloader = BasicBlockDownloader::new(client, consensus.clone()); + let use_hashed_state = provider.cached_storage_settings().use_hashed_state; let persistence_handle = PersistenceHandle::::spawn_service(provider, pruner, sync_metrics_tx); @@ -111,6 +112,7 @@ where engine_kind, evm_config, changeset_cache, + use_hashed_state, ); let engine_handler = EngineApiRequestHandler::new(to_tree_tx, from_tree); diff --git a/crates/engine/tree/Cargo.toml b/crates/engine/tree/Cargo.toml index 4968f28041..2edd073544 100644 --- a/crates/engine/tree/Cargo.toml +++ b/crates/engine/tree/Cargo.toml @@ -143,6 +143,13 @@ test-utils = [ "reth-evm-ethereum/test-utils", "reth-tasks/test-utils", ] +rocksdb = [ + "reth-provider/rocksdb", + "reth-prune/rocksdb", + "reth-stages?/rocksdb", + "reth-e2e-test-utils/rocksdb", +] +edge = ["rocksdb"] [[test]] name = "e2e_testsuite" diff --git a/crates/engine/tree/src/tree/cached_state.rs b/crates/engine/tree/src/tree/cached_state.rs index 2b85d70551..d61ce02152 100644 --- a/crates/engine/tree/src/tree/cached_state.rs +++ b/crates/engine/tree/src/tree/cached_state.rs @@ -351,6 +351,14 @@ impl StateProvider for CachedStateProvide self.state_provider.storage(account, storage_key) } } + + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: StorageKey, + ) -> ProviderResult> { + self.state_provider.storage_by_hashed_key(address, hashed_storage_key) + } } impl BytecodeReader for CachedStateProvider { diff --git a/crates/engine/tree/src/tree/instrumented_state.rs b/crates/engine/tree/src/tree/instrumented_state.rs index 02ab395dc3..99c61e533e 100644 --- a/crates/engine/tree/src/tree/instrumented_state.rs +++ b/crates/engine/tree/src/tree/instrumented_state.rs @@ -199,6 +199,17 @@ impl StateProvider for InstrumentedStateProvider { self.record_storage_fetch(start.elapsed()); res } + + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: StorageKey, + ) -> ProviderResult> { + let start = Instant::now(); + let res = self.state_provider.storage_by_hashed_key(address, hashed_storage_key); + self.record_storage_fetch(start.elapsed()); + res + } } impl BytecodeReader for InstrumentedStateProvider { diff --git a/crates/engine/tree/src/tree/mod.rs b/crates/engine/tree/src/tree/mod.rs index b1df776085..49bcacccde 100644 --- a/crates/engine/tree/src/tree/mod.rs +++ b/crates/engine/tree/src/tree/mod.rs @@ -32,7 +32,7 @@ use reth_provider::{ BlockExecutionOutput, BlockExecutionResult, BlockReader, ChangeSetReader, DatabaseProviderFactory, HashedPostStateProvider, ProviderError, StageCheckpointReader, StateProviderBox, StateProviderFactory, StateReader, StorageChangeSetReader, - TransactionVariant, + StorageSettingsCache, TransactionVariant, }; use reth_revm::database::StateProviderDatabase; use reth_stages_api::ControlFlow; @@ -271,6 +271,9 @@ where evm_config: C, /// Changeset cache for in-memory trie changesets changeset_cache: ChangesetCache, + /// Whether the node uses hashed state as canonical storage (v2 mode). + /// Cached at construction to avoid threading `StorageSettingsCache` bounds everywhere. + use_hashed_state: bool, } impl std::fmt::Debug @@ -296,6 +299,7 @@ where .field("engine_kind", &self.engine_kind) .field("evm_config", &self.evm_config) .field("changeset_cache", &self.changeset_cache) + .field("use_hashed_state", &self.use_hashed_state) .finish() } } @@ -313,7 +317,8 @@ where P::Provider: BlockReader + StageCheckpointReader + ChangeSetReader - + StorageChangeSetReader, + + StorageChangeSetReader + + StorageSettingsCache, C: ConfigureEvm + 'static, T: PayloadTypes>, V: EngineValidator, @@ -334,6 +339,7 @@ where engine_kind: EngineApiKind, evm_config: C, changeset_cache: ChangesetCache, + use_hashed_state: bool, ) -> Self { let (incoming_tx, incoming) = crossbeam_channel::unbounded(); @@ -355,6 +361,7 @@ where engine_kind, evm_config, changeset_cache, + use_hashed_state, } } @@ -375,6 +382,7 @@ where kind: EngineApiKind, evm_config: C, changeset_cache: ChangesetCache, + use_hashed_state: bool, ) -> (Sender, N::Block>>, UnboundedReceiver>) { let best_block_number = provider.best_block_number().unwrap_or(0); @@ -407,6 +415,7 @@ where kind, evm_config, changeset_cache, + use_hashed_state, ); let incoming = task.incoming_tx.clone(); spawn_os_thread("engine", || task.run()); @@ -2379,7 +2388,12 @@ where self.update_reorg_metrics(old.len(), old_first); self.reinsert_reorged_blocks(new.clone()); - self.reinsert_reorged_blocks(old.clone()); + + // When use_hashed_state is enabled, skip reinserting the old chain — the + // bundle state references plain state reverts which don't exist. + if !self.use_hashed_state { + self.reinsert_reorged_blocks(old.clone()); + } } // update the tracked in-memory state with the new chain diff --git a/crates/engine/tree/src/tree/payload_processor/multiproof.rs b/crates/engine/tree/src/tree/payload_processor/multiproof.rs index b4b896df4c..2d3fbc52c9 100644 --- a/crates/engine/tree/src/tree/payload_processor/multiproof.rs +++ b/crates/engine/tree/src/tree/payload_processor/multiproof.rs @@ -1541,6 +1541,7 @@ mod tests { providers::OverlayStateProviderFactory, test_utils::create_test_provider_factory, BlockNumReader, BlockReader, ChangeSetReader, DatabaseProviderFactory, LatestStateProvider, PruneCheckpointReader, StageCheckpointReader, StateProviderBox, StorageChangeSetReader, + StorageSettingsCache, }; use reth_trie::MultiProof; use reth_trie_db::ChangesetCache; @@ -1562,6 +1563,7 @@ mod tests { + PruneCheckpointReader + ChangeSetReader + StorageChangeSetReader + + StorageSettingsCache + BlockNumReader, > + Clone + Send @@ -1581,7 +1583,10 @@ mod tests { fn create_cached_provider(factory: F) -> CachedStateProvider where F: DatabaseProviderFactory< - Provider: BlockReader + StageCheckpointReader + PruneCheckpointReader, + Provider: BlockReader + + StageCheckpointReader + + PruneCheckpointReader + + reth_provider::StorageSettingsCache, > + Clone + Send + 'static, diff --git a/crates/engine/tree/src/tree/payload_validator.rs b/crates/engine/tree/src/tree/payload_validator.rs index d1c248178e..62228a6adc 100644 --- a/crates/engine/tree/src/tree/payload_validator.rs +++ b/crates/engine/tree/src/tree/payload_validator.rs @@ -38,7 +38,7 @@ use reth_provider::{ providers::OverlayStateProviderFactory, BlockExecutionOutput, BlockNumReader, BlockReader, ChangeSetReader, DatabaseProviderFactory, DatabaseProviderROFactory, HashedPostStateProvider, ProviderError, PruneCheckpointReader, StageCheckpointReader, StateProvider, - StateProviderFactory, StateReader, StorageChangeSetReader, + StateProviderFactory, StateReader, StorageChangeSetReader, StorageSettingsCache, }; use reth_revm::db::{states::bundle_state::BundleRetention, State}; use reth_trie::{updates::TrieUpdates, HashedPostState, StateRoot}; @@ -146,7 +146,8 @@ where + PruneCheckpointReader + ChangeSetReader + StorageChangeSetReader - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, > + BlockReader
+ ChangeSetReader + BlockNumReader @@ -1526,7 +1527,8 @@ where + PruneCheckpointReader + ChangeSetReader + StorageChangeSetReader - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, > + BlockReader
+ StateProviderFactory + StateReader diff --git a/crates/engine/tree/src/tree/tests.rs b/crates/engine/tree/src/tree/tests.rs index 7e376537e5..39e063dea6 100644 --- a/crates/engine/tree/src/tree/tests.rs +++ b/crates/engine/tree/src/tree/tests.rs @@ -221,6 +221,7 @@ impl TestHarness { EngineApiKind::Ethereum, evm_config, changeset_cache, + provider.cached_storage_settings().use_hashed_state, ); let block_builder = TestBlockBuilder::default().with_chain_spec((*chain_spec).clone()); diff --git a/crates/engine/tree/tests/e2e-testsuite/main.rs b/crates/engine/tree/tests/e2e-testsuite/main.rs index 20d7e2d68e..18bd1470a1 100644 --- a/crates/engine/tree/tests/e2e-testsuite/main.rs +++ b/crates/engine/tree/tests/e2e-testsuite/main.rs @@ -2,13 +2,15 @@ mod fcu_finalized_blocks; +use alloy_rpc_types_engine::PayloadStatusEnum; use eyre::Result; use reth_chainspec::{ChainSpecBuilder, MAINNET}; use reth_e2e_test_utils::testsuite::{ actions::{ - CaptureBlock, CompareNodeChainTips, CreateFork, ExpectFcuStatus, MakeCanonical, - ProduceBlocks, ProduceBlocksLocally, ProduceInvalidBlocks, ReorgTo, SelectActiveNode, - SendNewPayloads, UpdateBlockInfo, ValidateCanonicalTag, WaitForSync, + BlockReference, CaptureBlock, CompareNodeChainTips, CreateFork, ExpectFcuStatus, + MakeCanonical, ProduceBlocks, ProduceBlocksLocally, ProduceInvalidBlocks, ReorgTo, + SelectActiveNode, SendForkchoiceUpdate, SendNewPayloads, SetForkBase, UpdateBlockInfo, + ValidateCanonicalTag, WaitForSync, }, setup::{NetworkSetup, Setup}, TestBuilder, @@ -39,6 +41,14 @@ fn default_engine_tree_setup() -> Setup { ) } +/// Creates a v2 storage mode setup for engine tree e2e tests. +/// +/// v2 mode uses keccak256-hashed slot keys in static file changesets and rocksdb history +/// instead of plain keys in MDBX. +fn v2_engine_tree_setup() -> Setup { + default_engine_tree_setup().with_storage_v2() +} + /// Test that verifies forkchoice update and canonical chain insertion functionality. #[tokio::test] async fn test_engine_tree_fcu_canon_chain_insertion_e2e() -> Result<()> { @@ -334,3 +344,152 @@ async fn test_engine_tree_live_sync_transition_eventually_canonical_e2e() -> Res Ok(()) } + +// ==================== v2 storage mode variants ==================== + +/// v2 variant: Verifies forkchoice update and canonical chain insertion in v2 storage mode. +/// +/// Exercises the full `save_blocks` → `write_state` → static file changeset path with hashed keys. +#[tokio::test] +async fn test_engine_tree_fcu_canon_chain_insertion_v2_e2e() -> Result<()> { + reth_tracing::init_test_tracing(); + + let test = TestBuilder::new() + .with_setup(v2_engine_tree_setup()) + .with_action(ProduceBlocks::::new(1)) + .with_action(MakeCanonical::new()) + .with_action(ProduceBlocks::::new(3)) + .with_action(MakeCanonical::new()); + + test.run::().await?; + + Ok(()) +} + +/// v2 variant: Verifies forkchoice update with a reorg where all blocks are already available. +/// +/// Exercises `write_state_reverts` path with hashed changeset keys during CL-driven reorgs. +#[tokio::test] +async fn test_engine_tree_fcu_reorg_with_all_blocks_v2_e2e() -> Result<()> { + reth_tracing::init_test_tracing(); + + let test = TestBuilder::new() + .with_setup(v2_engine_tree_setup()) + .with_action(ProduceBlocks::::new(5)) + .with_action(MakeCanonical::new()) + .with_action(CreateFork::::new(2, 3)) + .with_action(CaptureBlock::new("fork_tip")) + .with_action(ReorgTo::::new_from_tag("fork_tip")); + + test.run::().await?; + + Ok(()) +} + +/// v2 variant: Verifies progressive canonical chain extension in v2 storage mode. +#[tokio::test] +async fn test_engine_tree_fcu_extends_canon_chain_v2_e2e() -> Result<()> { + reth_tracing::init_test_tracing(); + + let test = TestBuilder::new() + .with_setup(v2_engine_tree_setup()) + .with_action(ProduceBlocks::::new(1)) + .with_action(MakeCanonical::new()) + .with_action(ProduceBlocks::::new(10)) + .with_action(CaptureBlock::new("target_block")) + .with_action(ReorgTo::::new_from_tag("target_block")) + .with_action(MakeCanonical::new()); + + test.run::().await?; + + Ok(()) +} + +/// Creates a 2-node setup for disk-level reorg testing. +/// +/// Uses unconnected nodes so fork blocks can be produced independently on Node 1 and then +/// sent to Node 0 via newPayload only (no FCU), keeping Node 0's persisted chain intact +/// until the final `ReorgTo` triggers `find_disk_reorg`. +fn disk_reorg_setup(storage_v2: bool) -> Setup { + let mut setup = Setup::default() + .with_chain_spec(Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis( + serde_json::from_str(include_str!( + "../../../../e2e-test-utils/src/testsuite/assets/genesis.json" + )) + .unwrap(), + ) + .cancun_activated() + .build(), + )) + .with_network(NetworkSetup::multi_node_unconnected(2)) + .with_tree_config( + TreeConfig::default().with_legacy_state_root(false).with_has_enough_parallelism(true), + ); + if storage_v2 { + setup = setup.with_storage_v2(); + } + setup +} + +/// Builds a disk-level reorg test scenario. +/// +/// 1. Both nodes receive 3 shared blocks +/// 2. Node 0 extends to 10 blocks locally (persisted to disk) +/// 3. Node 1 builds an 8-block fork from block 3 (its canonical head) +/// 4. Fork blocks are sent to Node 0 via newPayload (no FCU, old chain stays on disk) +/// 5. FCU to fork tip on Node 0 triggers `find_disk_reorg` → `RemoveBlocksAbove(3)` +fn disk_reorg_test(storage_v2: bool) -> TestBuilder { + TestBuilder::new() + .with_setup(disk_reorg_setup(storage_v2)) + .with_action(SelectActiveNode::new(0)) + .with_action(ProduceBlocks::::new(3)) + .with_action(MakeCanonical::new()) + .with_action(ProduceBlocksLocally::::new(7)) + .with_action(MakeCanonical::with_active_node()) + .with_action(SelectActiveNode::new(1)) + .with_action(SetForkBase::new(3)) + .with_action(ProduceBlocksLocally::::new(8)) + .with_action(MakeCanonical::with_active_node()) + .with_action(CaptureBlock::new("fork_tip")) + .with_action( + SendNewPayloads::::new() + .with_source_node(1) + .with_target_node(0) + .with_start_block(4) + .with_total_blocks(8), + ) + .with_action( + SendForkchoiceUpdate::::new( + BlockReference::Tag("fork_tip".into()), + BlockReference::Tag("fork_tip".into()), + BlockReference::Tag("fork_tip".into()), + ) + .with_expected_status(PayloadStatusEnum::Valid) + .with_node_idx(0), + ) +} + +/// Verifies disk-level reorg in v1 (plain key) storage mode. +/// +/// Confirms `find_disk_reorg()` detects persisted blocks on the wrong fork and calls +/// `RemoveBlocksAbove` to truncate, then re-persists the correct fork chain. +#[tokio::test] +async fn test_engine_tree_disk_reorg_v1_e2e() -> Result<()> { + reth_tracing::init_test_tracing(); + disk_reorg_test(false).run::().await?; + Ok(()) +} + +/// v2 variant: Verifies disk-level reorg in v2 storage mode. +/// +/// Same scenario as v1 but with hashed changeset keys in static files and rocksdb history. +/// Exercises `find_disk_reorg()` → `RemoveBlocksAbove` with v2 hashed key format. +#[tokio::test] +async fn test_engine_tree_disk_reorg_v2_e2e() -> Result<()> { + reth_tracing::init_test_tracing(); + disk_reorg_test(true).run::().await?; + Ok(()) +} diff --git a/crates/exex/exex/src/backfill/test_utils.rs b/crates/exex/exex/src/backfill/test_utils.rs index 4d60929814..b40637df82 100644 --- a/crates/exex/exex/src/backfill/test_utils.rs +++ b/crates/exex/exex/src/backfill/test_utils.rs @@ -18,6 +18,7 @@ use reth_provider::{ }; use reth_revm::database::StateProviderDatabase; use reth_testing_utils::generators::sign_tx_with_key_pair; +use reth_trie_common::KeccakKeyHasher; use secp256k1::Keypair; pub(crate) fn to_execution_outcome( @@ -77,12 +78,9 @@ where let execution_outcome = to_execution_outcome(block.number(), &block_execution_output); // Commit the block's execution outcome to the database + let hashed_state = execution_outcome.hash_state_slow::().into_sorted(); let provider_rw = provider_factory.provider_rw()?; - provider_rw.append_blocks_with_state( - vec![block.clone()], - &execution_outcome, - Default::default(), - )?; + provider_rw.append_blocks_with_state(vec![block.clone()], &execution_outcome, hashed_state)?; provider_rw.commit()?; Ok(block_execution_output) @@ -210,11 +208,12 @@ where execution_outcome.state_mut().reverts.sort(); // Commit the block's execution outcome to the database + let hashed_state = execution_outcome.hash_state_slow::().into_sorted(); let provider_rw = provider_factory.provider_rw()?; provider_rw.append_blocks_with_state( vec![block1.clone(), block2.clone()], &execution_outcome, - Default::default(), + hashed_state, )?; provider_rw.commit()?; diff --git a/crates/node/core/src/args/mod.rs b/crates/node/core/src/args/mod.rs index 85ea66df40..c839c55373 100644 --- a/crates/node/core/src/args/mod.rs +++ b/crates/node/core/src/args/mod.rs @@ -80,7 +80,7 @@ pub use static_files::{StaticFilesArgs, MINIMAL_BLOCKS_PER_FILE}; mod rocksdb; pub use rocksdb::{RocksDbArgs, RocksDbArgsError}; -/// `StorageArgs` for configuring storage mode (v2 vs v1/legacy). +/// `StorageArgs` for configuring storage settings. mod storage; pub use storage::StorageArgs; diff --git a/crates/node/core/src/args/storage.rs b/crates/node/core/src/args/storage.rs index f69ae53391..f1aa37fd50 100644 --- a/crates/node/core/src/args/storage.rs +++ b/crates/node/core/src/args/storage.rs @@ -1,11 +1,13 @@ -//! clap [Args](clap::Args) for storage mode configuration +//! clap [Args](clap::Args) for storage configuration use clap::{ArgAction, Args}; -/// Parameters for storage mode configuration. +/// Parameters for storage configuration. /// /// This controls whether the node uses v2 storage defaults (with `RocksDB` and static file /// optimizations) or v1/legacy storage defaults. +/// +/// Individual storage settings can be overridden with `--static-files.*` and `--rocksdb.*` flags. #[derive(Debug, Args, PartialEq, Eq, Clone, Copy, Default)] #[command(next_help_heading = "Storage")] pub struct StorageArgs { @@ -40,21 +42,24 @@ mod tests { use super::*; use clap::Parser; + /// A helper type to parse Args more easily #[derive(Parser)] - struct CommandParser { + struct CommandParser { #[command(flatten)] - args: StorageArgs, + args: T, } #[test] fn test_default_storage_args() { - let args = CommandParser::parse_from(["reth"]).args; + let default_args = StorageArgs::default(); + let args = CommandParser::::parse_from(["reth"]).args; + assert_eq!(args, default_args); assert!(!args.v2); } #[test] fn test_parse_v2_flag() { - let args = CommandParser::parse_from(["reth", "--storage.v2"]).args; + let args = CommandParser::::parse_from(["reth", "--storage.v2"]).args; assert!(args.v2); } } diff --git a/crates/node/core/src/node_config.rs b/crates/node/core/src/node_config.rs index 48e87cf05d..bedd9217fe 100644 --- a/crates/node/core/src/node_config.rs +++ b/crates/node/core/src/node_config.rs @@ -155,7 +155,7 @@ pub struct NodeConfig { /// All `RocksDB` table routing arguments pub rocksdb: RocksDbArgs, - /// Storage mode configuration (v2 vs v1/legacy) + /// All storage related arguments with --storage prefix pub storage: StorageArgs, } @@ -355,6 +355,12 @@ impl NodeConfig { self } + /// Set the storage args for the node + pub const fn with_storage(mut self, storage: StorageArgs) -> Self { + self.storage = storage; + self + } + /// Returns pruning configuration. pub fn prune_config(&self) -> Option where @@ -398,6 +404,13 @@ impl NodeConfig { s = s.with_use_hashed_state(self.storage.use_hashed_state); + if s.use_hashed_state { + s = s.with_storage_changesets_in_static_files(true); + } + if s.storage_changesets_in_static_files { + s = s.with_use_hashed_state(true); + } + s } diff --git a/crates/primitives-traits/src/lib.rs b/crates/primitives-traits/src/lib.rs index 0cf2df35e1..e9caa35517 100644 --- a/crates/primitives-traits/src/lib.rs +++ b/crates/primitives-traits/src/lib.rs @@ -164,7 +164,7 @@ pub use alloy_primitives::{logs_bloom, Log, LogData}; pub mod proofs; mod storage; -pub use storage::{StorageEntry, ValueWithSubKey}; +pub use storage::{StorageEntry, StorageSlotKey, ValueWithSubKey}; pub mod sync; diff --git a/crates/primitives-traits/src/storage.rs b/crates/primitives-traits/src/storage.rs index 4383f03cf9..91d69815a3 100644 --- a/crates/primitives-traits/src/storage.rs +++ b/crates/primitives-traits/src/storage.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{B256, U256}; +use alloy_primitives::{keccak256, B256, U256}; /// Trait for `DupSort` table values that contain a subkey. /// @@ -12,6 +12,117 @@ pub trait ValueWithSubKey { fn get_subkey(&self) -> Self::SubKey; } +/// A storage slot key that tracks whether it holds a plain (unhashed) EVM slot +/// or a keccak256-hashed slot. +/// +/// This enum replaces the `use_hashed_state: bool` parameter pattern by carrying +/// provenance with the key itself. Once tagged at a read/write boundary, downstream +/// code can call [`Self::to_hashed`] without risk of double-hashing — hashing a +/// [`StorageSlotKey::Hashed`] is a no-op. +/// +/// The on-disk encoding is unchanged (raw 32-byte [`B256`]). The variant is set +/// by the code that knows the context (which table, which storage mode). +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum StorageSlotKey { + /// An unhashed EVM storage slot, as produced by REVM execution. + Plain(B256), + /// A keccak256-hashed storage slot, as stored in `HashedStorages` and + /// in v2-mode `StorageChangeSets`. + Hashed(B256), +} + +impl Default for StorageSlotKey { + fn default() -> Self { + Self::Plain(B256::ZERO) + } +} + +impl StorageSlotKey { + /// Create a plain slot key from a REVM [`U256`] storage index. + pub const fn from_u256(slot: U256) -> Self { + Self::Plain(B256::new(slot.to_be_bytes())) + } + + /// Create a plain slot key from a raw [`B256`]. + pub const fn plain(key: B256) -> Self { + Self::Plain(key) + } + + /// Create a hashed slot key from a raw [`B256`]. + pub const fn hashed(key: B256) -> Self { + Self::Hashed(key) + } + + /// Tag a raw [`B256`] based on the storage mode. + /// + /// When `use_hashed_state` is true the key is assumed already hashed. + /// When false it is assumed to be a plain slot. + pub const fn from_raw(key: B256, use_hashed_state: bool) -> Self { + if use_hashed_state { + Self::Hashed(key) + } else { + Self::Plain(key) + } + } + + /// Returns the raw [`B256`] regardless of variant. + pub const fn as_b256(&self) -> B256 { + match *self { + Self::Plain(b) | Self::Hashed(b) => b, + } + } + + /// Returns `true` if this key is already hashed. + pub const fn is_hashed(&self) -> bool { + matches!(self, Self::Hashed(_)) + } + + /// Returns `true` if this key is plain (unhashed). + pub const fn is_plain(&self) -> bool { + matches!(self, Self::Plain(_)) + } + + /// Produce the keccak256-hashed form of this slot key. + /// + /// - If already [`Hashed`](Self::Hashed), returns the inner value as-is (no double-hash). + /// - If [`Plain`](Self::Plain), applies keccak256 and returns the result. + pub fn to_hashed(&self) -> B256 { + match *self { + Self::Hashed(b) => b, + Self::Plain(b) => keccak256(b), + } + } + + /// Convert a plain slot to its changeset representation. + /// + /// In v2 mode (`use_hashed_state = true`), the changeset stores hashed keys, + /// so the plain key is hashed. In v1 mode, the plain key is stored as-is. + /// + /// Panics (debug) if called on an already-hashed key. + pub fn to_changeset_key(self, use_hashed_state: bool) -> B256 { + debug_assert!(self.is_plain(), "to_changeset_key called on already-hashed key"); + if use_hashed_state { + self.to_hashed() + } else { + self.as_b256() + } + } + + /// Like [`to_changeset_key`](Self::to_changeset_key) but returns a tagged + /// [`StorageSlotKey`] instead of a raw [`B256`]. + /// + /// Panics (debug) if called on an already-hashed key. + pub fn to_changeset(self, use_hashed_state: bool) -> Self { + Self::from_raw(self.to_changeset_key(use_hashed_state), use_hashed_state) + } +} + +impl From for B256 { + fn from(key: StorageSlotKey) -> Self { + key.as_b256() + } +} + /// Account storage entry. /// /// `key` is the subkey when used as a value in the `StorageChangeSets` table. @@ -31,6 +142,14 @@ impl StorageEntry { pub const fn new(key: B256, value: U256) -> Self { Self { key, value } } + + /// Tag this entry's key as a [`StorageSlotKey`] based on the storage mode. + /// + /// When `use_hashed_state` is true, the key is tagged as already-hashed. + /// When false, it is tagged as plain. + pub const fn slot_key(&self, use_hashed_state: bool) -> StorageSlotKey { + StorageSlotKey::from_raw(self.key, use_hashed_state) + } } impl ValueWithSubKey for StorageEntry { diff --git a/crates/prune/prune/src/segments/user/storage_history.rs b/crates/prune/prune/src/segments/user/storage_history.rs index 58a1d62106..57a1ba7274 100644 --- a/crates/prune/prune/src/segments/user/storage_history.rs +++ b/crates/prune/prune/src/segments/user/storage_history.rs @@ -135,7 +135,7 @@ impl StorageHistory { let (block_address, entry) = result?; let block_number = block_address.block_number(); let address = block_address.address(); - highest_deleted_storages.insert((address, entry.key), block_number); + highest_deleted_storages.insert((address, entry.key.as_b256()), block_number); last_changeset_pruned_block = Some(block_number); pruned_changesets += 1; limiter.increment_deleted_entries_count(); @@ -273,7 +273,7 @@ impl StorageHistory { let (block_address, entry) = result?; let block_number = block_address.block_number(); let address = block_address.address(); - highest_deleted_storages.insert((address, entry.key), block_number); + highest_deleted_storages.insert((address, entry.key.as_b256()), block_number); last_changeset_pruned_block = Some(block_number); changesets_processed += 1; limiter.increment_deleted_entries_count(); diff --git a/crates/revm/src/test_utils.rs b/crates/revm/src/test_utils.rs index b268808461..409101fa6b 100644 --- a/crates/revm/src/test_utils.rs +++ b/crates/revm/src/test_utils.rs @@ -160,6 +160,14 @@ impl StateProvider for StateProviderTest { ) -> ProviderResult> { Ok(self.accounts.get(&account).and_then(|(storage, _)| storage.get(&storage_key).copied())) } + + fn storage_by_hashed_key( + &self, + _address: Address, + _hashed_storage_key: StorageKey, + ) -> ProviderResult> { + Ok(None) + } } impl BytecodeReader for StateProviderTest { diff --git a/crates/rpc/rpc-eth-types/src/cache/db.rs b/crates/rpc/rpc-eth-types/src/cache/db.rs index 09e1b3db3c..8bfc091ce5 100644 --- a/crates/rpc/rpc-eth-types/src/cache/db.rs +++ b/crates/rpc/rpc-eth-types/src/cache/db.rs @@ -154,6 +154,14 @@ impl StateProvider for StateProviderTraitObjWrapper { self.0.storage(account, storage_key) } + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: alloy_primitives::StorageKey, + ) -> reth_errors::ProviderResult> { + self.0.storage_by_hashed_key(address, hashed_storage_key) + } + fn account_code( &self, addr: &Address, diff --git a/crates/stages/stages/src/stages/execution.rs b/crates/stages/stages/src/stages/execution.rs index f1901dce46..12a7cade7b 100644 --- a/crates/stages/stages/src/stages/execution.rs +++ b/crates/stages/stages/src/stages/execution.rs @@ -22,6 +22,7 @@ use reth_stages_api::{ UnwindInput, UnwindOutput, }; use reth_static_file_types::StaticFileSegment; +use reth_trie::KeccakKeyHasher; use std::{ cmp::{max, Ordering}, collections::BTreeMap, @@ -461,9 +462,16 @@ where } } - // write output + // Write output. When `use_hashed_state` is enabled, `write_state` skips writing to + // plain account/storage tables and only writes bytecodes and changesets. The hashed + // state is then written separately below. provider.write_state(&state, OriginalValuesKnown::Yes, StateWriteConfig::default())?; + if provider.cached_storage_settings().use_hashed_state { + let hashed_state = state.hash_state_slow::(); + provider.write_hashed_state(&hashed_state.into_sorted())?; + } + let db_write_duration = time.elapsed(); debug!( target: "sync::stages::execution", diff --git a/crates/stages/stages/src/stages/hashing_account.rs b/crates/stages/stages/src/stages/hashing_account.rs index f38b638405..ab367bd50f 100644 --- a/crates/stages/stages/src/stages/hashing_account.rs +++ b/crates/stages/stages/src/stages/hashing_account.rs @@ -9,7 +9,9 @@ use reth_db_api::{ }; use reth_etl::Collector; use reth_primitives_traits::Account; -use reth_provider::{AccountExtReader, DBProvider, HashingWriter, StatsReader}; +use reth_provider::{ + AccountExtReader, DBProvider, HashingWriter, StatsReader, StorageSettingsCache, +}; use reth_stages_api::{ AccountHashingCheckpoint, EntitiesCheckpoint, ExecInput, ExecOutput, Stage, StageCheckpoint, StageError, StageId, UnwindInput, UnwindOutput, @@ -134,7 +136,11 @@ impl Default for AccountHashingStage { impl Stage for AccountHashingStage where - Provider: DBProvider + HashingWriter + AccountExtReader + StatsReader, + Provider: DBProvider + + HashingWriter + + AccountExtReader + + StatsReader + + StorageSettingsCache, { /// Return the id of the stage fn id(&self) -> StageId { @@ -142,11 +148,21 @@ where } /// Execute the stage. + /// + /// When `use_hashed_state` is enabled, this stage is a no-op because the execution stage + /// writes directly to `HashedAccounts`. Otherwise, it hashes plain state to populate hashed + /// tables. fn execute(&mut self, provider: &Provider, input: ExecInput) -> Result { if input.target_reached() { return Ok(ExecOutput::done(input.checkpoint())) } + // If using hashed state as canonical, execution already writes to `HashedAccounts`, + // so this stage becomes a no-op. + if provider.cached_storage_settings().use_hashed_state { + return Ok(ExecOutput::done(input.checkpoint().with_block_number(input.target()))); + } + let (from_block, to_block) = input.next_block_range().into_inner(); // if there are more blocks then threshold it is faster to go over Plain state and hash all @@ -234,10 +250,14 @@ where provider: &Provider, input: UnwindInput, ) -> Result { + // NOTE: this runs in both v1 and v2 mode. In v2 mode, execution writes + // directly to `HashedAccounts`, but the unwind must still revert those + // entries here because `MerkleUnwind` runs after this stage (in unwind + // order) and needs `HashedAccounts` to reflect the target block state + // before it can verify the state root. let (range, unwind_progress, _) = input.unwind_block_range_with_threshold(self.commit_threshold); - // Aggregate all transition changesets and make a list of accounts that have been changed. provider.unwind_account_hashing_range(range)?; let mut stage_checkpoint = diff --git a/crates/stages/stages/src/stages/hashing_storage.rs b/crates/stages/stages/src/stages/hashing_storage.rs index 19e8936209..5186280fb0 100644 --- a/crates/stages/stages/src/stages/hashing_storage.rs +++ b/crates/stages/stages/src/stages/hashing_storage.rs @@ -15,6 +15,7 @@ use reth_stages_api::{ EntitiesCheckpoint, ExecInput, ExecOutput, Stage, StageCheckpoint, StageError, StageId, StorageHashingCheckpoint, UnwindInput, UnwindOutput, }; +use reth_storage_api::StorageSettingsCache; use reth_storage_errors::provider::ProviderResult; use std::{ fmt::Debug, @@ -68,7 +69,11 @@ impl Default for StorageHashingStage { impl Stage for StorageHashingStage where - Provider: DBProvider + StorageReader + HashingWriter + StatsReader, + Provider: DBProvider + + StorageReader + + HashingWriter + + StatsReader + + StorageSettingsCache, { /// Return the id of the stage fn id(&self) -> StageId { @@ -82,6 +87,12 @@ where return Ok(ExecOutput::done(input.checkpoint())) } + // If use_hashed_state is enabled, execution writes directly to `HashedStorages`, + // so this stage becomes a no-op. + if provider.cached_storage_settings().use_hashed_state { + return Ok(ExecOutput::done(input.checkpoint().with_block_number(input.target()))); + } + let (from_block, to_block) = input.next_block_range().into_inner(); // if there are more blocks then threshold it is faster to go over Plain state and hash all @@ -176,6 +187,11 @@ where provider: &Provider, input: UnwindInput, ) -> Result { + // NOTE: this runs in both v1 and v2 mode. In v2 mode, execution writes + // directly to `HashedStorages`, but the unwind must still revert those + // entries here because `MerkleUnwind` runs after this stage (in unwind + // order) and needs `HashedStorages` to reflect the target block state + // before it can verify the state root. let (range, unwind_progress, _) = input.unwind_block_range_with_threshold(self.commit_threshold); diff --git a/crates/stages/stages/src/stages/merkle.rs b/crates/stages/stages/src/stages/merkle.rs index cd9d5ebd43..d7871aa8d3 100644 --- a/crates/stages/stages/src/stages/merkle.rs +++ b/crates/stages/stages/src/stages/merkle.rs @@ -9,7 +9,7 @@ use reth_db_api::{ use reth_primitives_traits::{GotExpected, SealedHeader}; use reth_provider::{ ChangeSetReader, DBProvider, HeaderProvider, ProviderError, StageCheckpointReader, - StageCheckpointWriter, StatsReader, StorageChangeSetReader, TrieWriter, + StageCheckpointWriter, StatsReader, StorageChangeSetReader, StorageSettingsCache, TrieWriter, }; use reth_stages_api::{ BlockErrorKind, EntitiesCheckpoint, ExecInput, ExecOutput, MerkleCheckpoint, Stage, @@ -160,6 +160,7 @@ where + HeaderProvider + ChangeSetReader + StorageChangeSetReader + + StorageSettingsCache + StageCheckpointReader + StageCheckpointWriter, { diff --git a/crates/stages/stages/src/stages/utils.rs b/crates/stages/stages/src/stages/utils.rs index 2d9e4035f2..0f8392d9bd 100644 --- a/crates/stages/stages/src/stages/utils.rs +++ b/crates/stages/stages/src/stages/utils.rs @@ -208,7 +208,10 @@ where for (idx, changeset_result) in walker.enumerate() { let (BlockNumberAddress((block_number, address)), storage) = changeset_result?; - cache.entry(AddressStorageKey((address, storage.key))).or_default().push(block_number); + cache + .entry(AddressStorageKey((address, storage.key.as_b256()))) + .or_default() + .push(block_number); if idx > 0 && idx % interval == 0 && total_changesets > 1000 { info!(target: "sync::stages::index_history", progress = %format!("{:.4}%", (idx as f64 / total_changesets as f64) * 100.0), "Collecting indices"); diff --git a/crates/stages/stages/tests/pipeline.rs b/crates/stages/stages/tests/pipeline.rs index 3b4dc7bb21..2bee26201d 100644 --- a/crates/stages/stages/tests/pipeline.rs +++ b/crates/stages/stages/tests/pipeline.rs @@ -3,7 +3,7 @@ use alloy_consensus::{constants::ETH_TO_WEI, Header, TxEip1559, TxReceipt}; use alloy_eips::eip1559::INITIAL_BASE_FEE; use alloy_genesis::{Genesis, GenesisAccount}; -use alloy_primitives::{bytes, Address, Bytes, TxKind, B256, U256}; +use alloy_primitives::{bytes, keccak256, Address, Bytes, TxKind, B256, U256}; use reth_chainspec::{ChainSpecBuilder, ChainSpecProvider, MAINNET}; use reth_config::config::StageConfig; use reth_consensus::noop::NoopConsensus; @@ -36,7 +36,7 @@ use reth_stages::sets::DefaultStages; use reth_stages_api::{Pipeline, StageId}; use reth_static_file::StaticFileProducer; use reth_storage_api::{ - ChangeSetReader, StateProvider, StorageChangeSetReader, StorageSettingsCache, + ChangeSetReader, StateProvider, StorageChangeSetReader, StorageSettings, StorageSettingsCache, }; use reth_testing_utils::generators::{self, generate_key, sign_tx_with_key_pair}; use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot}; @@ -89,6 +89,11 @@ fn assert_changesets_queryable( "storage changesets should be queryable from static files for blocks {:?}", block_range ); + + // Verify keys are in hashed format (v2 mode) + for (_, entry) in &storage_changesets { + assert!(entry.key.is_hashed(), "v2: storage changeset keys should be tagged as hashed"); + } } else { let storage_changesets: Vec<_> = provider .tx_ref() @@ -100,6 +105,16 @@ fn assert_changesets_queryable( "storage changesets should be queryable from MDBX for blocks {:?}", block_range ); + + // Verify keys are plain (not hashed) in v1 mode + for (_, entry) in &storage_changesets { + let key = entry.key; + assert_ne!( + key, + keccak256(key), + "v1: storage changeset key should be plain (not its own keccak256)" + ); + } } // Verify account changesets @@ -201,19 +216,22 @@ where pipeline } -/// Tests pipeline with ALL stages enabled using both ETH transfers and contract storage changes. +/// Shared helper for pipeline forward sync and unwind tests. /// -/// This test: /// 1. Pre-funds a signer account and deploys a Counter contract in genesis /// 2. Each block contains two transactions: /// - ETH transfer to a recipient (account state changes) /// - Counter `increment()` call (storage state changes) /// 3. Runs the full pipeline with ALL stages enabled -/// 4. Forward syncs to block 5, unwinds to block 2 +/// 4. Forward syncs to `num_blocks`, unwinds to `unwind_target`, then re-syncs back to `num_blocks` /// -/// This exercises both account and storage hashing/history stages. -#[tokio::test(flavor = "multi_thread")] -async fn test_pipeline() -> eyre::Result<()> { +/// When `storage_settings` is `Some`, the pipeline provider factory is configured with the given +/// settings before genesis initialization (e.g. v2 storage mode). +async fn run_pipeline_forward_and_unwind( + storage_settings: Option, + num_blocks: u64, + unwind_target: u64, +) -> eyre::Result<()> { reth_tracing::init_test_tracing(); // Generate a keypair for signing transactions @@ -259,7 +277,6 @@ async fn test_pipeline() -> eyre::Result<()> { let evm_config = EthEvmConfig::new(chain_spec.clone()); // Build blocks by actually executing transactions to get correct state roots - let num_blocks = 5u64; let mut blocks: Vec> = Vec::new(); let mut parent_hash = genesis.hash(); @@ -384,11 +401,15 @@ async fn test_pipeline() -> eyre::Result<()> { // This is needed because we wrote state during block generation for computing state roots let pipeline_provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); + if let Some(settings) = storage_settings { + pipeline_provider_factory.set_storage_settings_cache(settings); + } init_genesis(&pipeline_provider_factory).expect("init genesis"); let pipeline_genesis = pipeline_provider_factory.sealed_header(0)?.expect("genesis should exist"); let pipeline_consensus = NoopConsensus::arc(); + let blocks_clone = blocks.clone(); let file_client = create_file_client_from_blocks(blocks); let max_block = file_client.max_block().unwrap(); let tip = file_client.tip().expect("tip"); @@ -417,7 +438,7 @@ async fn test_pipeline() -> eyre::Result<()> { { let provider = pipeline_provider_factory.provider()?; let last_block = provider.last_block_number()?; - assert_eq!(last_block, 5, "should have synced 5 blocks"); + assert_eq!(last_block, num_blocks, "should have synced {num_blocks} blocks"); for stage_id in [ StageId::Headers, @@ -435,29 +456,28 @@ async fn test_pipeline() -> eyre::Result<()> { let checkpoint = provider.get_stage_checkpoint(stage_id)?; assert_eq!( checkpoint.map(|c| c.block_number), - Some(5), - "{stage_id} checkpoint should be at block 5" + Some(num_blocks), + "{stage_id} checkpoint should be at block {num_blocks}" ); } // Verify the counter contract's storage was updated - // After 5 blocks with 1 increment each, slot 0 should be 5 + // After num_blocks blocks with 1 increment each, slot 0 should be num_blocks let state = provider.latest(); let counter_storage = state.storage(CONTRACT_ADDRESS, B256::ZERO)?; assert_eq!( counter_storage, - Some(U256::from(5)), - "Counter storage slot 0 should be 5 after 5 increments" + Some(U256::from(num_blocks)), + "Counter storage slot 0 should be {num_blocks} after {num_blocks} increments" ); } // Verify changesets are queryable before unwind // This validates that the #21561 fix works - unwind needs to read changesets from the correct // source - assert_changesets_queryable(&pipeline_provider_factory, 1..=5)?; + assert_changesets_queryable(&pipeline_provider_factory, 1..=num_blocks)?; - // Unwind to block 2 - let unwind_target = 2u64; + // Unwind to unwind_target pipeline.unwind(unwind_target, None)?; // Verify unwind @@ -484,7 +504,114 @@ async fn test_pipeline() -> eyre::Result<()> { ); } } + + let state = provider.latest(); + let counter_storage = state.storage(CONTRACT_ADDRESS, B256::ZERO)?; + assert_eq!( + counter_storage, + Some(U256::from(unwind_target)), + "Counter storage slot 0 should be {unwind_target} after unwinding to block {unwind_target}" + ); + } + + // Re-sync: build a new pipeline starting from unwind_target and sync back to num_blocks + let resync_file_client = create_file_client_from_blocks(blocks_clone); + let resync_consensus = NoopConsensus::arc(); + let resync_stages_config = StageConfig::default(); + + let unwind_head = pipeline_provider_factory + .sealed_header(unwind_target)? + .expect("unwind target header should exist"); + + let mut resync_header_downloader = + ReverseHeadersDownloaderBuilder::new(resync_stages_config.headers) + .build(resync_file_client.clone(), resync_consensus.clone()) + .into_task(); + resync_header_downloader.update_local_head(unwind_head); + resync_header_downloader.update_sync_target(SyncTarget::Tip(tip)); + + let mut resync_body_downloader = BodiesDownloaderBuilder::new(resync_stages_config.bodies) + .build(resync_file_client, resync_consensus, pipeline_provider_factory.clone()) + .into_task(); + resync_body_downloader + .set_download_range(unwind_target + 1..=max_block) + .expect("set download range"); + + let resync_pipeline = build_pipeline( + pipeline_provider_factory.clone(), + resync_header_downloader, + resync_body_downloader, + max_block, + tip, + ); + + let (_resync_pipeline, resync_result) = resync_pipeline.run_as_fut(None).await; + resync_result?; + + // Verify re-sync + { + let provider = pipeline_provider_factory.provider()?; + let last_block = provider.last_block_number()?; + assert_eq!(last_block, num_blocks, "should have re-synced to {num_blocks} blocks"); + + for stage_id in [ + StageId::Headers, + StageId::Bodies, + StageId::SenderRecovery, + StageId::Execution, + StageId::AccountHashing, + StageId::StorageHashing, + StageId::MerkleExecute, + StageId::TransactionLookup, + StageId::IndexAccountHistory, + StageId::IndexStorageHistory, + StageId::Finish, + ] { + let checkpoint = provider.get_stage_checkpoint(stage_id)?; + assert_eq!( + checkpoint.map(|c| c.block_number), + Some(num_blocks), + "{stage_id} checkpoint should be at block {num_blocks} after re-sync" + ); + } + + let state = provider.latest(); + let counter_storage = state.storage(CONTRACT_ADDRESS, B256::ZERO)?; + assert_eq!( + counter_storage, + Some(U256::from(num_blocks)), + "Counter storage slot 0 should be {num_blocks} after re-sync" + ); } Ok(()) } + +/// Tests pipeline with ALL stages enabled using both ETH transfers and contract storage changes. +/// +/// This test: +/// 1. Pre-funds a signer account and deploys a Counter contract in genesis +/// 2. Each block contains two transactions: +/// - ETH transfer to a recipient (account state changes) +/// - Counter `increment()` call (storage state changes) +/// 3. Runs the full pipeline with ALL stages enabled +/// 4. Forward syncs to block 5, unwinds to block 2, then re-syncs to block 5 +/// +/// This exercises both account and storage hashing/history stages. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline() -> eyre::Result<()> { + run_pipeline_forward_and_unwind(None, 5, 2).await +} + +/// Same as [`test_pipeline`] but runs with v2 storage settings (`use_hashed_state=true`, +/// `storage_changesets_in_static_files=true`, etc.). +/// +/// In v2 mode: +/// - The execution stage writes directly to `HashedAccounts`/`HashedStorages` +/// - `AccountHashingStage` and `StorageHashingStage` are no-ops during forward execution +/// - Changesets are stored in static files with pre-hashed storage keys +/// - Unwind must still revert hashed state via the hashing stages before `MerkleUnwind` validates +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2() -> eyre::Result<()> { + run_pipeline_forward_and_unwind(Some(StorageSettings::v2()), 5, 2).await +} diff --git a/crates/storage/db-api/src/models/metadata.rs b/crates/storage/db-api/src/models/metadata.rs index 14b9cb44ae..ef67dba722 100644 --- a/crates/storage/db-api/src/models/metadata.rs +++ b/crates/storage/db-api/src/models/metadata.rs @@ -43,11 +43,19 @@ pub struct StorageSettings { impl StorageSettings { /// Returns the default base `StorageSettings`. /// - /// Always returns [`Self::v1()`]. Use the `--storage.v2` CLI flag to opt into - /// [`Self::v2()`] at runtime. The `rocksdb` feature only makes the v2 backend - /// *available*; it does not activate it by default. + /// When the `edge` feature is enabled, returns [`Self::v2()`] so that CI and + /// edge builds automatically use v2 storage defaults. Otherwise returns + /// [`Self::v1()`]. The `--storage.v2` CLI flag can also opt into v2 at runtime + /// regardless of feature flags. pub const fn base() -> Self { - Self::v1() + #[cfg(feature = "edge")] + { + Self::v2() + } + #[cfg(not(feature = "edge"))] + { + Self::v1() + } } /// Creates `StorageSettings` for v2 nodes with all storage features enabled: @@ -65,7 +73,7 @@ impl StorageSettings { storages_history_in_rocksdb: true, transaction_hash_numbers_in_rocksdb: true, account_history_in_rocksdb: true, - use_hashed_state: false, + use_hashed_state: true, } } diff --git a/crates/storage/provider/src/changeset_walker.rs b/crates/storage/provider/src/changeset_walker.rs index 5eb521e3a7..ba4fe4811c 100644 --- a/crates/storage/provider/src/changeset_walker.rs +++ b/crates/storage/provider/src/changeset_walker.rs @@ -5,8 +5,7 @@ use crate::ProviderResult; use alloy_primitives::BlockNumber; use reth_db::models::AccountBeforeTx; use reth_db_api::models::BlockNumberAddress; -use reth_primitives_traits::StorageEntry; -use reth_storage_api::{ChangeSetReader, StorageChangeSetReader}; +use reth_storage_api::{ChangeSetReader, ChangesetEntry, StorageChangeSetReader}; use std::ops::{Bound, RangeBounds}; /// Iterator that walks account changesets from static files in a block range. @@ -110,7 +109,7 @@ pub struct StaticFileStorageChangesetWalker

{ /// Current block being processed current_block: BlockNumber, /// Changesets for current block - current_changesets: Vec<(BlockNumberAddress, StorageEntry)>, + current_changesets: Vec<(BlockNumberAddress, ChangesetEntry)>, /// Index within current block's changesets changeset_index: usize, } @@ -144,7 +143,7 @@ impl

Iterator for StaticFileStorageChangesetWalker

where P: StorageChangeSetReader, { - type Item = ProviderResult<(BlockNumberAddress, StorageEntry)>; + type Item = ProviderResult<(BlockNumberAddress, ChangesetEntry)>; fn next(&mut self) -> Option { if let Some(changeset) = self.current_changesets.get(self.changeset_index).copied() { diff --git a/crates/storage/provider/src/providers/blockchain_provider.rs b/crates/storage/provider/src/providers/blockchain_provider.rs index a9cf4c38f4..63de5fd845 100644 --- a/crates/storage/provider/src/providers/blockchain_provider.rs +++ b/crates/storage/provider/src/providers/blockchain_provider.rs @@ -23,11 +23,13 @@ use reth_chainspec::ChainInfo; use reth_db_api::models::{AccountBeforeTx, BlockNumberAddress, StoredBlockBodyIndices}; use reth_execution_types::ExecutionOutcome; use reth_node_types::{BlockTy, HeaderTy, NodeTypesWithDB, ReceiptTy, TxTy}; -use reth_primitives_traits::{Account, RecoveredBlock, SealedHeader, StorageEntry}; +use reth_primitives_traits::{Account, RecoveredBlock, SealedHeader}; use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; -use reth_storage_api::{BlockBodyIndicesProvider, NodePrimitivesProvider, StorageChangeSetReader}; +use reth_storage_api::{ + BlockBodyIndicesProvider, ChangesetEntry, NodePrimitivesProvider, StorageChangeSetReader, +}; use reth_storage_errors::provider::ProviderResult; use reth_trie::{HashedPostState, KeccakKeyHasher}; use revm_database::BundleState; @@ -713,7 +715,7 @@ impl StorageChangeSetReader for BlockchainProvider { fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { self.consistent_provider()?.storage_changeset(block_number) } @@ -722,14 +724,14 @@ impl StorageChangeSetReader for BlockchainProvider { block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { self.consistent_provider()?.get_storage_before_block(block_number, address, storage_key) } fn storage_changesets_range( &self, range: impl RangeBounds, - ) -> ProviderResult> { + ) -> ProviderResult> { self.consistent_provider()?.storage_changesets_range(range) } diff --git a/crates/storage/provider/src/providers/consistent.rs b/crates/storage/provider/src/providers/consistent.rs index 4963708d1b..7ff3b5db19 100644 --- a/crates/storage/provider/src/providers/consistent.rs +++ b/crates/storage/provider/src/providers/consistent.rs @@ -21,13 +21,16 @@ use reth_chainspec::ChainInfo; use reth_db_api::models::{AccountBeforeTx, BlockNumberAddress, StoredBlockBodyIndices}; use reth_execution_types::{BundleStateInit, ExecutionOutcome, RevertsInit}; use reth_node_types::{BlockTy, HeaderTy, ReceiptTy, TxTy}; -use reth_primitives_traits::{Account, BlockBody, RecoveredBlock, SealedHeader, StorageEntry}; +use reth_primitives_traits::{ + Account, BlockBody, RecoveredBlock, SealedHeader, StorageEntry, StorageSlotKey, +}; use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; use reth_storage_api::{ - BlockBodyIndicesProvider, DatabaseProviderFactory, NodePrimitivesProvider, StateProvider, - StateProviderBox, StorageChangeSetReader, TryIntoHistoricalStateProvider, + BlockBodyIndicesProvider, ChangesetEntry, DatabaseProviderFactory, NodePrimitivesProvider, + StateProvider, StateProviderBox, StorageChangeSetReader, StorageSettingsCache, + TryIntoHistoricalStateProvider, }; use reth_storage_errors::provider::ProviderResult; use revm_database::states::PlainStorageRevert; @@ -214,13 +217,16 @@ impl ConsistentProvider { ))) } - /// Populate a [`BundleStateInit`] and [`RevertsInit`] using cursors over the - /// [`reth_db::PlainAccountState`] and [`reth_db::PlainStorageState`] tables, based on the given - /// storage and account changesets. + /// Populate a [`BundleStateInit`] and [`RevertsInit`] based on the given storage and account + /// changesets. + /// + /// When `use_hashed_state` is enabled, storage changeset keys are already hashed, so current + /// values are read directly from [`reth_db_api::tables::HashedStorages`]. Otherwise, values + /// are read via [`StateProvider::storage`] which queries plain state tables. fn populate_bundle_state( &self, account_changeset: Vec<(u64, AccountBeforeTx)>, - storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>, + storage_changeset: Vec<(BlockNumberAddress, ChangesetEntry)>, block_range_end: BlockNumber, ) -> ProviderResult<(BundleStateInit, RevertsInit)> { let mut state: BundleStateInit = HashMap::default(); @@ -257,10 +263,16 @@ impl ConsistentProvider { }; // match storage. - match account_state.2.entry(old_storage.key) { + match account_state.2.entry(old_storage.key.as_b256()) { hash_map::Entry::Vacant(entry) => { - let new_storage_value = - state_provider.storage(address, old_storage.key)?.unwrap_or_default(); + let new_storage_value = match old_storage.key { + StorageSlotKey::Hashed(_) => state_provider + .storage_by_hashed_key(address, old_storage.key.as_b256())? + .unwrap_or_default(), + StorageSlotKey::Plain(_) => state_provider + .storage(address, old_storage.key.as_b256())? + .unwrap_or_default(), + }; entry.insert((old_storage.value, new_storage_value)); } hash_map::Entry::Occupied(mut entry) => { @@ -274,7 +286,7 @@ impl ConsistentProvider { .entry(address) .or_default() .1 - .push(old_storage); + .push(StorageEntry::from(old_storage)); } Ok((state, reverts)) @@ -1300,7 +1312,8 @@ impl StorageChangeSetReader for ConsistentProvider { fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { + let use_hashed = self.storage_provider.cached_storage_settings().use_hashed_state; if let Some(state) = self.head_block.as_ref().and_then(|b| b.block_on_chain(block_number.into())) { @@ -1316,9 +1329,10 @@ impl StorageChangeSetReader for ConsistentProvider { .flatten() .flat_map(|revert: PlainStorageRevert| { revert.storage_revert.into_iter().map(move |(key, value)| { + let tagged_key = StorageSlotKey::from_u256(key).to_changeset(use_hashed); ( BlockNumberAddress((block_number, revert.address)), - StorageEntry { key: key.into(), value: value.to_previous_value() }, + ChangesetEntry { key: tagged_key, value: value.to_previous_value() }, ) }) }) @@ -1353,7 +1367,8 @@ impl StorageChangeSetReader for ConsistentProvider { block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { + let use_hashed = self.storage_provider.cached_storage_settings().use_hashed_state; if let Some(state) = self.head_block.as_ref().and_then(|b| b.block_on_chain(block_number.into())) { @@ -1372,9 +1387,11 @@ impl StorageChangeSetReader for ConsistentProvider { return None } revert.storage_revert.into_iter().find_map(|(key, value)| { - let key = key.into(); - (key == storage_key) - .then(|| StorageEntry { key, value: value.to_previous_value() }) + let tagged_key = StorageSlotKey::from_u256(key).to_changeset(use_hashed); + (tagged_key.as_b256() == storage_key).then(|| ChangesetEntry { + key: tagged_key, + value: value.to_previous_value(), + }) }) }); Ok(changeset) @@ -1398,12 +1415,14 @@ impl StorageChangeSetReader for ConsistentProvider { fn storage_changesets_range( &self, range: impl RangeBounds, - ) -> ProviderResult> { + ) -> ProviderResult> { let range = to_range(range); let mut changesets = Vec::new(); let database_start = range.start; let mut database_end = range.end; + let use_hashed = self.storage_provider.cached_storage_settings().use_hashed_state; + if let Some(head_block) = &self.head_block { database_end = head_block.anchor().number; @@ -1421,9 +1440,14 @@ impl StorageChangeSetReader for ConsistentProvider { .flatten() .flat_map(|revert: PlainStorageRevert| { revert.storage_revert.into_iter().map(move |(key, value)| { + let tagged_key = + StorageSlotKey::from_u256(key).to_changeset(use_hashed); ( BlockNumberAddress((state.number(), revert.address)), - StorageEntry { key: key.into(), value: value.to_previous_value() }, + ChangesetEntry { + key: tagged_key, + value: value.to_previous_value(), + }, ) }) }); @@ -2060,4 +2084,648 @@ mod tests { Ok(()) } + + #[test] + fn test_get_state_storage_value_hashed_state() -> eyre::Result<()> { + use alloy_primitives::{keccak256, U256}; + use reth_db_api::{models::StorageSettings, tables, transaction::DbTxMut}; + use reth_primitives_traits::StorageEntry; + use reth_storage_api::StorageSettingsCache; + use std::collections::HashMap; + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + let slot_b256 = B256::from(slot); + let hashed_address = keccak256(address); + let hashed_slot = keccak256(slot_b256); + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let blocks = random_block_range( + &mut rng, + 0..=1, + BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() }, + ); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(100))); + s + })], + [ + Vec::new(), + vec![(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])], + ], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + + provider_rw.tx_ref().put::( + hashed_address, + StorageEntry { key: hashed_slot, value: U256::from(100) }, + )?; + provider_rw.tx_ref().put::(hashed_address, account)?; + + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + let consistent_provider = provider.consistent_provider()?; + + let outcome = + consistent_provider.get_state(1..=1)?.expect("should return execution outcome"); + + let state = &outcome.bundle.state; + let account_state = state.get(&address).expect("should have account in bundle state"); + let storage = &account_state.storage; + + let slot_as_u256 = U256::from_be_bytes(*hashed_slot); + let storage_slot = storage.get(&slot_as_u256).expect("should have the slot in storage"); + + assert_eq!( + storage_slot.present_value, + U256::from(100), + "present_value should be 100 (the actual value in HashedStorages)" + ); + + Ok(()) + } + + #[test] + #[cfg(all(unix, feature = "rocksdb"))] + fn test_get_state_storage_value_hashed_state_historical() -> eyre::Result<()> { + use alloy_primitives::{keccak256, U256}; + use reth_db_api::{models::StorageSettings, tables, transaction::DbTxMut}; + use reth_primitives_traits::StorageEntry; + use reth_storage_api::StorageSettingsCache; + use std::collections::HashMap; + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + let slot_b256 = B256::from(slot); + let hashed_address = keccak256(address); + let hashed_slot = keccak256(slot_b256); + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let blocks = random_block_range( + &mut rng, + 0..=3, + BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() }, + ); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(300))); + s + })], + [ + Vec::new(), + vec![(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])], + vec![(address, Some(Some(account.into())), vec![(slot, U256::from(100))])], + vec![(address, Some(Some(account.into())), vec![(slot, U256::from(200))])], + ], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + + provider_rw.tx_ref().put::( + hashed_address, + StorageEntry { key: hashed_slot, value: U256::from(300) }, + )?; + provider_rw.tx_ref().put::(hashed_address, account)?; + + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + let consistent_provider = provider.consistent_provider()?; + + let outcome = + consistent_provider.get_state(1..=2)?.expect("should return execution outcome"); + + let state = &outcome.bundle.state; + let account_state = state.get(&address).expect("should have account in bundle state"); + let storage = &account_state.storage; + + let slot_as_u256 = U256::from_be_bytes(*hashed_slot); + let storage_slot = storage.get(&slot_as_u256).expect("should have the slot in storage"); + + assert_eq!( + storage_slot.present_value, + U256::from(200), + "present_value should be 200 (the value at block 2, not 300 which is the latest)" + ); + + Ok(()) + } + + #[test] + fn test_get_state_storage_value_plain_state() -> eyre::Result<()> { + use alloy_primitives::U256; + use reth_db_api::{models::StorageSettings, tables, transaction::DbTxMut}; + use reth_primitives_traits::StorageEntry; + use reth_storage_api::StorageSettingsCache; + use std::collections::HashMap; + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + let slot_b256 = B256::from(slot); + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v1()); + + let blocks = random_block_range( + &mut rng, + 0..=1, + BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() }, + ); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(100))); + s + })], + [ + Vec::new(), + vec![(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])], + ], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + + provider_rw.tx_ref().put::( + address, + StorageEntry { key: slot_b256, value: U256::from(100) }, + )?; + provider_rw.tx_ref().put::(address, account)?; + + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + let consistent_provider = provider.consistent_provider()?; + + let outcome = + consistent_provider.get_state(1..=1)?.expect("should return execution outcome"); + + let state = &outcome.bundle.state; + let account_state = state.get(&address).expect("should have account in bundle state"); + let storage = &account_state.storage; + + let storage_slot = storage.get(&slot).expect("should have the slot in storage"); + + assert_eq!( + storage_slot.present_value, + U256::from(100), + "present_value should be 100 (the actual value in PlainStorageState)" + ); + + Ok(()) + } + + #[test] + fn test_storage_changeset_consistent_keys_hashed_state() -> eyre::Result<()> { + use alloy_primitives::{keccak256, U256}; + use reth_db_api::models::StorageSettings; + use reth_storage_api::{StorageChangeSetReader, StorageSettingsCache}; + use std::collections::HashMap; + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let (database_blocks, in_memory_blocks) = random_blocks(&mut rng, 1, 1, None, None, 0..1); + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + database_blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(100))); + s + })], + [[(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])]], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + + let in_mem_block = in_memory_blocks.first().unwrap(); + let senders = in_mem_block.senders().expect("failed to recover senders"); + let chain = NewCanonicalChain::Commit { + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + in_mem_block.clone(), + senders, + )), + execution_output: Arc::new(BlockExecutionOutput { + state: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::from(100), U256::from(200))); + s + })], + [[(address, Some(Some(account.into())), vec![(slot, U256::from(100))])]], + [], + ), + result: BlockExecutionResult { + receipts: Default::default(), + requests: Default::default(), + gas_used: 0, + blob_gas_used: 0, + }, + }), + ..Default::default() + }], + }; + provider.canonical_in_memory_state.update_chain(chain); + + let consistent_provider = provider.consistent_provider()?; + + let db_changeset = consistent_provider.storage_changeset(0)?; + let mem_changeset = consistent_provider.storage_changeset(1)?; + + let slot_b256 = B256::from(slot); + let _hashed_slot_b256 = keccak256(slot_b256); + + assert_eq!(db_changeset.len(), 1); + assert_eq!(mem_changeset.len(), 1); + + let db_key = db_changeset[0].1.key; + let mem_key = mem_changeset[0].1.key; + + assert_eq!( + db_key, mem_key, + "DB and in-memory changesets should return the same key format (hashed) for the same logical slot" + ); + + Ok(()) + } + + #[test] + fn test_storage_changeset_consistent_keys_plain_state() -> eyre::Result<()> { + use alloy_primitives::U256; + use reth_db_api::models::StorageSettings; + use reth_storage_api::{StorageChangeSetReader, StorageSettingsCache}; + use std::collections::HashMap; + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v1()); + + let (database_blocks, in_memory_blocks) = random_blocks(&mut rng, 1, 1, None, None, 0..1); + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + database_blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(100))); + s + })], + [[(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])]], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + + let in_mem_block = in_memory_blocks.first().unwrap(); + let senders = in_mem_block.senders().expect("failed to recover senders"); + let chain = NewCanonicalChain::Commit { + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + in_mem_block.clone(), + senders, + )), + execution_output: Arc::new(BlockExecutionOutput { + state: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::from(100), U256::from(200))); + s + })], + [[(address, Some(Some(account.into())), vec![(slot, U256::from(100))])]], + [], + ), + result: BlockExecutionResult { + receipts: Default::default(), + requests: Default::default(), + gas_used: 0, + blob_gas_used: 0, + }, + }), + ..Default::default() + }], + }; + provider.canonical_in_memory_state.update_chain(chain); + + let consistent_provider = provider.consistent_provider()?; + + let db_changeset = consistent_provider.storage_changeset(0)?; + let mem_changeset = consistent_provider.storage_changeset(1)?; + + let slot_b256 = B256::from(slot); + + assert_eq!(db_changeset.len(), 1); + assert_eq!(mem_changeset.len(), 1); + + let db_key = db_changeset[0].1.key.as_b256(); + let mem_key = mem_changeset[0].1.key.as_b256(); + + assert_eq!(db_key, slot_b256, "DB changeset should use plain (unhashed) key"); + assert_eq!(mem_key, slot_b256, "In-memory changeset should use plain (unhashed) key"); + assert_eq!( + db_key, mem_key, + "DB and in-memory changesets should return the same key format (plain) for the same logical slot" + ); + + Ok(()) + } + + #[test] + fn test_storage_changesets_range_consistent_keys_hashed_state() -> eyre::Result<()> { + use alloy_primitives::U256; + use reth_db_api::models::StorageSettings; + use reth_storage_api::{StorageChangeSetReader, StorageSettingsCache}; + use std::collections::HashMap; + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let (database_blocks, in_memory_blocks) = random_blocks(&mut rng, 2, 1, None, None, 0..1); + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + database_blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(100))); + s + })], + vec![ + vec![(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])], + vec![], + ], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + + let in_mem_block = in_memory_blocks.first().unwrap(); + let senders = in_mem_block.senders().expect("failed to recover senders"); + let chain = NewCanonicalChain::Commit { + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + in_mem_block.clone(), + senders, + )), + execution_output: Arc::new(BlockExecutionOutput { + state: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::from(100), U256::from(200))); + s + })], + [[(address, Some(Some(account.into())), vec![(slot, U256::from(100))])]], + [], + ), + result: BlockExecutionResult { + receipts: Default::default(), + requests: Default::default(), + gas_used: 0, + blob_gas_used: 0, + }, + }), + ..Default::default() + }], + }; + provider.canonical_in_memory_state.update_chain(chain); + + let consistent_provider = provider.consistent_provider()?; + + let all_changesets = consistent_provider.storage_changesets_range(0..=2)?; + + assert_eq!(all_changesets.len(), 2, "should have one changeset entry per block"); + + let keys: Vec = all_changesets.iter().map(|(_, entry)| entry.key.as_b256()).collect(); + + assert_eq!( + keys[0], keys[1], + "same logical slot should produce identical keys whether from DB or memory" + ); + + Ok(()) + } + + #[test] + fn test_storage_changesets_range_consistent_keys_plain_state() -> eyre::Result<()> { + use alloy_primitives::U256; + use reth_db_api::models::StorageSettings; + use reth_storage_api::{StorageChangeSetReader, StorageSettingsCache}; + use std::collections::HashMap; + + let mut rng = generators::rng(); + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v1()); + + let (database_blocks, in_memory_blocks) = random_blocks(&mut rng, 2, 1, None, None, 0..1); + + let address = alloy_primitives::Address::with_last_byte(1); + let account = reth_primitives_traits::Account { + nonce: 1, + balance: U256::from(1000), + bytecode_hash: None, + }; + let slot = U256::from(0x42); + + let provider_rw = factory.provider_rw()?; + provider_rw.append_blocks_with_state( + database_blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { + bundle: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::ZERO, U256::from(100))); + s + })], + vec![ + vec![(address, Some(Some(account.into())), vec![(slot, U256::ZERO)])], + vec![], + ], + [], + ), + first_block: 0, + ..Default::default() + }, + Default::default(), + )?; + provider_rw.commit()?; + + let provider = BlockchainProvider::new(factory)?; + + let in_mem_block = in_memory_blocks.first().unwrap(); + let senders = in_mem_block.senders().expect("failed to recover senders"); + let chain = NewCanonicalChain::Commit { + new: vec![ExecutedBlock { + recovered_block: Arc::new(RecoveredBlock::new_sealed( + in_mem_block.clone(), + senders, + )), + execution_output: Arc::new(BlockExecutionOutput { + state: BundleState::new( + [(address, None, Some(account.into()), { + let mut s = HashMap::default(); + s.insert(slot, (U256::from(100), U256::from(200))); + s + })], + [[(address, Some(Some(account.into())), vec![(slot, U256::from(100))])]], + [], + ), + result: BlockExecutionResult { + receipts: Default::default(), + requests: Default::default(), + gas_used: 0, + blob_gas_used: 0, + }, + }), + ..Default::default() + }], + }; + provider.canonical_in_memory_state.update_chain(chain); + + let consistent_provider = provider.consistent_provider()?; + + let all_changesets = consistent_provider.storage_changesets_range(0..=2)?; + + assert_eq!(all_changesets.len(), 2, "should have one changeset entry per block"); + + let slot_b256 = B256::from(slot); + let keys: Vec = all_changesets.iter().map(|(_, entry)| entry.key.as_b256()).collect(); + + assert_eq!( + keys[0], keys[1], + "same logical slot should produce identical keys whether from DB or memory" + ); + assert_eq!( + keys[0], slot_b256, + "keys should be plain/unhashed when use_hashed_state is false" + ); + + Ok(()) + } } diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index 5990f3cf62..2513719e58 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -52,6 +52,7 @@ use reth_execution_types::{BlockExecutionOutput, BlockExecutionResult, Chain, Ex use reth_node_types::{BlockTy, BodyTy, HeaderTy, NodeTypes, ReceiptTy, TxTy}; use reth_primitives_traits::{ Account, Block as _, BlockBody as _, Bytecode, RecoveredBlock, SealedHeader, StorageEntry, + StorageSlotKey, }; use reth_prune_types::{ PruneCheckpoint, PruneMode, PruneModes, PruneSegment, MINIMUM_UNWIND_SAFE_DISTANCE, @@ -59,7 +60,7 @@ use reth_prune_types::{ use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; use reth_storage_api::{ - BlockBodyIndicesProvider, BlockBodyReader, MetadataProvider, MetadataWriter, + BlockBodyIndicesProvider, BlockBodyReader, ChangesetEntry, MetadataProvider, MetadataWriter, NodePrimitivesProvider, StateProvider, StateWriteConfig, StorageChangeSetReader, StorageSettingsCache, TryIntoHistoricalStateProvider, WriteStateInput, }; @@ -1186,7 +1187,7 @@ impl DatabaseProvider { fn populate_bundle_state( &self, account_changeset: Vec<(u64, AccountBeforeTx)>, - storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>, + storage_changeset: Vec<(BlockNumberAddress, ChangesetEntry)>, plain_accounts_cursor: &mut A, plain_storage_cursor: &mut S, ) -> ProviderResult<(BundleStateInit, RevertsInit)> @@ -1236,11 +1237,12 @@ impl DatabaseProvider { }; // match storage. - match account_state.2.entry(old_storage.key) { + let storage_key = old_storage.key.as_b256(); + match account_state.2.entry(storage_key) { hash_map::Entry::Vacant(entry) => { let new_storage = plain_storage_cursor - .seek_by_key_subkey(address, old_storage.key)? - .filter(|storage| storage.key == old_storage.key) + .seek_by_key_subkey(address, storage_key)? + .filter(|storage| storage.key == storage_key) .unwrap_or_default(); entry.insert((old_storage.value, new_storage.value)); } @@ -1255,7 +1257,78 @@ impl DatabaseProvider { .entry(address) .or_default() .1 - .push(old_storage); + .push(old_storage.into_storage_entry()); + } + + Ok((state, reverts)) + } + + /// Like [`populate_bundle_state`](Self::populate_bundle_state), but reads current values from + /// `HashedAccounts`/`HashedStorages`. Addresses are hashed via `keccak256` for DB lookups, + /// while storage keys from changesets are assumed to already be hashed and are used as-is. + /// The output `BundleStateInit`/`RevertsInit` structures remain keyed by plain address. + fn populate_bundle_state_hashed( + &self, + account_changeset: Vec<(u64, AccountBeforeTx)>, + storage_changeset: Vec<(BlockNumberAddress, ChangesetEntry)>, + hashed_accounts_cursor: &mut impl DbCursorRO, + hashed_storage_cursor: &mut impl DbDupCursorRO, + ) -> ProviderResult<(BundleStateInit, RevertsInit)> { + let mut state: BundleStateInit = HashMap::default(); + let mut reverts: RevertsInit = HashMap::default(); + + // add account changeset changes + for (block_number, account_before) in account_changeset.into_iter().rev() { + let AccountBeforeTx { info: old_info, address } = account_before; + match state.entry(address) { + hash_map::Entry::Vacant(entry) => { + let hashed_address = keccak256(address); + let new_info = + hashed_accounts_cursor.seek_exact(hashed_address)?.map(|kv| kv.1); + entry.insert((old_info, new_info, HashMap::default())); + } + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().0 = old_info; + } + } + reverts.entry(block_number).or_default().entry(address).or_default().0 = Some(old_info); + } + + // add storage changeset changes + for (block_and_address, old_storage) in storage_changeset.into_iter().rev() { + let BlockNumberAddress((block_number, address)) = block_and_address; + let account_state = match state.entry(address) { + hash_map::Entry::Vacant(entry) => { + let hashed_address = keccak256(address); + let present_info = + hashed_accounts_cursor.seek_exact(hashed_address)?.map(|kv| kv.1); + entry.insert((present_info, present_info, HashMap::default())) + } + hash_map::Entry::Occupied(entry) => entry.into_mut(), + }; + + let storage_key = old_storage.key.as_b256(); + match account_state.2.entry(storage_key) { + hash_map::Entry::Vacant(entry) => { + let hashed_address = keccak256(address); + let new_storage = hashed_storage_cursor + .seek_by_key_subkey(hashed_address, storage_key)? + .filter(|storage| storage.key == storage_key) + .unwrap_or_default(); + entry.insert((old_storage.value, new_storage.value)); + } + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().0 = old_storage.value; + } + }; + + reverts + .entry(block_number) + .or_default() + .entry(address) + .or_default() + .1 + .push(old_storage.into_storage_entry()); } Ok((state, reverts)) @@ -1323,7 +1396,12 @@ impl DatabaseProvider { impl AccountReader for DatabaseProvider { fn basic_account(&self, address: &Address) -> ProviderResult> { - Ok(self.tx.get_by_encoded_key::(address)?) + if self.cached_storage_settings().use_hashed_state { + let hashed_address = keccak256(address); + Ok(self.tx.get_by_encoded_key::(&hashed_address)?) + } else { + Ok(self.tx.get_by_encoded_key::(address)?) + } } } @@ -1341,11 +1419,24 @@ impl AccountExtReader for DatabaseProvider, ) -> ProviderResult)>> { - let mut plain_accounts = self.tx.cursor_read::()?; - Ok(iter - .into_iter() - .map(|address| plain_accounts.seek_exact(address).map(|a| (address, a.map(|(_, v)| v)))) - .collect::, _>>()?) + if self.cached_storage_settings().use_hashed_state { + let mut hashed_accounts = self.tx.cursor_read::()?; + Ok(iter + .into_iter() + .map(|address| { + let hashed_address = keccak256(address); + hashed_accounts.seek_exact(hashed_address).map(|a| (address, a.map(|(_, v)| v))) + }) + .collect::, _>>()?) + } else { + let mut plain_accounts = self.tx.cursor_read::()?; + Ok(iter + .into_iter() + .map(|address| { + plain_accounts.seek_exact(address).map(|a| (address, a.map(|(_, v)| v))) + }) + .collect::, _>>()?) + } } fn changed_accounts_and_blocks_with_range( @@ -1397,7 +1488,7 @@ impl StorageChangeSetReader for DatabaseProvider fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { if self.cached_storage_settings().storage_changesets_in_static_files { self.static_file_provider.storage_changeset(block_number) } else { @@ -1406,7 +1497,16 @@ impl StorageChangeSetReader for DatabaseProvider self.tx .cursor_dup_read::()? .walk_range(storage_range)? - .map(|r| r.map_err(Into::into)) + .map(|r| { + let (bna, entry) = r?; + Ok(( + bna, + ChangesetEntry { + key: StorageSlotKey::plain(entry.key), + value: entry.value, + }, + )) + }) .collect() } } @@ -1416,30 +1516,42 @@ impl StorageChangeSetReader for DatabaseProvider block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { if self.cached_storage_settings().storage_changesets_in_static_files { self.static_file_provider.get_storage_before_block(block_number, address, storage_key) } else { - self.tx + Ok(self + .tx .cursor_dup_read::()? .seek_by_key_subkey(BlockNumberAddress((block_number, address)), storage_key)? .filter(|entry| entry.key == storage_key) - .map(Ok) - .transpose() + .map(|entry| ChangesetEntry { + key: StorageSlotKey::plain(entry.key), + value: entry.value, + })) } } fn storage_changesets_range( &self, range: impl RangeBounds, - ) -> ProviderResult> { + ) -> ProviderResult> { if self.cached_storage_settings().storage_changesets_in_static_files { self.static_file_provider.storage_changesets_range(range) } else { self.tx .cursor_dup_read::()? .walk_range(BlockNumberAddressRange::from(range))? - .map(|r| r.map_err(Into::into)) + .map(|r| { + let (bna, entry) = r?; + Ok(( + bna, + ChangesetEntry { + key: StorageSlotKey::plain(entry.key), + value: entry.value, + }, + )) + }) .collect() } } @@ -2131,23 +2243,47 @@ impl StorageReader for DatabaseProvider &self, addresses_with_keys: impl IntoIterator)>, ) -> ProviderResult)>> { - let mut plain_storage = self.tx.cursor_dup_read::()?; + if self.cached_storage_settings().use_hashed_state { + let mut hashed_storage = self.tx.cursor_dup_read::()?; - addresses_with_keys - .into_iter() - .map(|(address, storage)| { - storage - .into_iter() - .map(|key| -> ProviderResult<_> { - Ok(plain_storage - .seek_by_key_subkey(address, key)? - .filter(|v| v.key == key) - .unwrap_or_else(|| StorageEntry { key, value: Default::default() })) - }) - .collect::>>() - .map(|storage| (address, storage)) - }) - .collect::>>() + addresses_with_keys + .into_iter() + .map(|(address, storage)| { + let hashed_address = keccak256(address); + storage + .into_iter() + .map(|key| -> ProviderResult<_> { + let hashed_key = keccak256(key); + let value = hashed_storage + .seek_by_key_subkey(hashed_address, hashed_key)? + .filter(|v| v.key == hashed_key) + .map(|v| v.value) + .unwrap_or_default(); + Ok(StorageEntry { key, value }) + }) + .collect::>>() + .map(|storage| (address, storage)) + }) + .collect::>>() + } else { + let mut plain_storage = self.tx.cursor_dup_read::()?; + + addresses_with_keys + .into_iter() + .map(|(address, storage)| { + storage + .into_iter() + .map(|key| -> ProviderResult<_> { + Ok(plain_storage + .seek_by_key_subkey(address, key)? + .filter(|v| v.key == key) + .unwrap_or_else(|| StorageEntry { key, value: Default::default() })) + }) + .collect::>>() + .map(|storage| (address, storage)) + }) + .collect::>>() + } } fn changed_storages_with_range( @@ -2159,7 +2295,7 @@ impl StorageReader for DatabaseProvider BTreeMap::new(), |mut accounts: BTreeMap>, entry| { let (BlockNumberAddress((_, address)), storage_entry) = entry; - accounts.entry(address).or_default().insert(storage_entry.key); + accounts.entry(address).or_default().insert(storage_entry.key.as_b256()); Ok(accounts) }, ) @@ -2189,7 +2325,7 @@ impl StorageReader for DatabaseProvider BTreeMap::new(), |mut storages: BTreeMap<(Address, B256), Vec>, (index, storage)| { storages - .entry((index.address(), storage.key)) + .entry((index.address(), storage.key.as_b256())) .or_default() .push(index.block_number()); Ok(storages) @@ -2334,11 +2470,15 @@ impl StateWriter first_block: BlockNumber, config: StateWriteConfig, ) -> ProviderResult<()> { + let use_hashed_state = self.cached_storage_settings().use_hashed_state; + // Write storage changes if config.write_storage_changesets { tracing::trace!("Writing storage changes"); let mut storages_cursor = self.tx_ref().cursor_dup_write::()?; + let mut hashed_storages_cursor = + self.tx_ref().cursor_dup_write::()?; for (block_index, mut storage_changes) in reverts.storage.into_iter().enumerate() { let block_number = first_block + block_index as BlockNumber; @@ -2351,22 +2491,40 @@ impl StateWriter for PlainStorageRevert { address, wiped, storage_revert } in storage_changes { let mut storage = storage_revert .into_iter() - .map(|(k, v)| (B256::new(k.to_be_bytes()), v)) + .map(|(k, v)| { + (StorageSlotKey::from_u256(k).to_changeset_key(use_hashed_state), v) + }) .collect::>(); // sort storage slots by key. storage.par_sort_unstable_by_key(|a| a.0); - // If we are writing the primary storage wipe transition, the pre-existing plain + // If we are writing the primary storage wipe transition, the pre-existing // 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 + // When `use_hashed_state` is enabled, we read from `HashedStorages` + // instead of `PlainStorageState`. The hashed entries already have + // `keccak256(slot)` keys which is exactly the format needed for hashed + // changesets (static file changesets always use hashed keys when + // `use_hashed_state` is true). + // + // 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"); - if let Some((_, entry)) = storages_cursor.seek_exact(address)? { + if use_hashed_state { + let hashed_address = keccak256(address); + if let Some((_, entry)) = + hashed_storages_cursor.seek_exact(hashed_address)? + { + wiped_storage.push((entry.key, entry.value)); + while let Some(entry) = hashed_storages_cursor.next_dup_val()? { + wiped_storage.push((entry.key, entry.value)) + } + } + } else if let Some((_, entry)) = storages_cursor.seek_exact(address)? { wiped_storage.push((entry.key, entry.value)); while let Some(entry) = storages_cursor.next_dup_val()? { wiped_storage.push((entry.key, entry.value)) @@ -2414,17 +2572,54 @@ impl StateWriter changes.storage.par_sort_by_key(|a| a.address); changes.contracts.par_sort_by_key(|a| a.0); - // Write new account state - tracing::trace!(len = changes.accounts.len(), "Writing new account state"); - let mut accounts_cursor = self.tx_ref().cursor_write::()?; - // write account to database. - for (address, account) in changes.accounts { - if let Some(account) = account { - tracing::trace!(?address, "Updating plain state account"); - accounts_cursor.upsert(address, &account.into())?; - } else if accounts_cursor.seek_exact(address)?.is_some() { - tracing::trace!(?address, "Deleting plain state account"); - accounts_cursor.delete_current()?; + // When use_hashed_state is enabled, skip plain state writes for accounts and storage. + // The hashed state is already written by the separate `write_hashed_state()` call. + // Bytecode writes remain unconditional since Bytecodes is not a plain/hashed table. + if !self.cached_storage_settings().use_hashed_state { + // Write new account state + tracing::trace!(len = changes.accounts.len(), "Writing new account state"); + let mut accounts_cursor = self.tx_ref().cursor_write::()?; + // write account to database. + for (address, account) in changes.accounts { + if let Some(account) = account { + tracing::trace!(?address, "Updating plain state account"); + accounts_cursor.upsert(address, &account.into())?; + } else if accounts_cursor.seek_exact(address)?.is_some() { + tracing::trace!(?address, "Deleting plain state account"); + accounts_cursor.delete_current()?; + } + } + + // Write new storage state and wipe storage if needed. + tracing::trace!(len = changes.storage.len(), "Writing new storage state"); + let mut storages_cursor = + self.tx_ref().cursor_dup_write::()?; + for PlainStorageChangeset { address, wipe_storage, storage } in changes.storage { + // Wiping of storage. + if wipe_storage && storages_cursor.seek_exact(address)?.is_some() { + storages_cursor.delete_current_duplicates()?; + } + // cast storages to B256. + let mut storage = storage + .into_iter() + .map(|(k, value)| StorageEntry { key: k.into(), value }) + .collect::>(); + // sort storage slots by key. + storage.par_sort_unstable_by_key(|a| a.key); + + for entry in storage { + tracing::trace!(?address, ?entry.key, "Updating plain state storage"); + if let Some(db_entry) = + storages_cursor.seek_by_key_subkey(address, entry.key)? && + db_entry.key == entry.key + { + storages_cursor.delete_current()?; + } + + if !entry.value.is_zero() { + storages_cursor.upsert(address, &entry)?; + } + } } } @@ -2435,36 +2630,6 @@ impl StateWriter bytecodes_cursor.upsert(hash, &Bytecode(bytecode))?; } - // Write new storage state and wipe storage if needed. - tracing::trace!(len = changes.storage.len(), "Writing new storage state"); - let mut storages_cursor = self.tx_ref().cursor_dup_write::()?; - for PlainStorageChangeset { address, wipe_storage, storage } in changes.storage { - // Wiping of storage. - if wipe_storage && storages_cursor.seek_exact(address)?.is_some() { - storages_cursor.delete_current_duplicates()?; - } - // cast storages to B256. - let mut storage = storage - .into_iter() - .map(|(k, value)| StorageEntry { key: k.into(), value }) - .collect::>(); - // sort storage slots by key. - storage.par_sort_unstable_by_key(|a| a.key); - - for entry in storage { - tracing::trace!(?address, ?entry.key, "Updating plain state storage"); - if let Some(db_entry) = storages_cursor.seek_by_key_subkey(address, entry.key)? && - db_entry.key == entry.key - { - storages_cursor.delete_current()?; - } - - if !entry.value.is_zero() { - storages_cursor.upsert(address, &entry)?; - } - } - } - Ok(()) } @@ -2553,6 +2718,11 @@ impl StateWriter changesets } else { self.take::(storage_range)? + .into_iter() + .map(|(k, v)| { + (k, ChangesetEntry { key: StorageSlotKey::plain(v.key), value: v.value }) + }) + .collect() }; let account_changeset = if self.cached_storage_settings().account_changesets_in_static_files { @@ -2565,47 +2735,85 @@ impl StateWriter self.take::(range)? }; - // This is not working for blocks that are not at tip. as plain state is not the last - // state of end range. We should rename the functions or add support to access - // History state. Accessing history state can be tricky but we are not gaining - // anything. - let mut plain_accounts_cursor = self.tx.cursor_write::()?; - let mut plain_storage_cursor = self.tx.cursor_dup_write::()?; + if self.cached_storage_settings().use_hashed_state { + let mut hashed_accounts_cursor = self.tx.cursor_write::()?; + let mut hashed_storage_cursor = self.tx.cursor_dup_write::()?; - let (state, _) = self.populate_bundle_state( - account_changeset, - storage_changeset, - &mut plain_accounts_cursor, - &mut plain_storage_cursor, - )?; + let (state, _) = self.populate_bundle_state_hashed( + account_changeset, + storage_changeset, + &mut hashed_accounts_cursor, + &mut hashed_storage_cursor, + )?; - // iterate over local plain state remove all account and all storages. - for (address, (old_account, new_account, storage)) in &state { - // revert account if needed. - if old_account != new_account { - let existing_entry = plain_accounts_cursor.seek_exact(*address)?; - if let Some(account) = old_account { - plain_accounts_cursor.upsert(*address, account)?; - } else if existing_entry.is_some() { - plain_accounts_cursor.delete_current()?; + for (address, (old_account, new_account, storage)) in &state { + if old_account != new_account { + let hashed_address = keccak256(address); + let existing_entry = hashed_accounts_cursor.seek_exact(hashed_address)?; + if let Some(account) = old_account { + hashed_accounts_cursor.upsert(hashed_address, account)?; + } else if existing_entry.is_some() { + hashed_accounts_cursor.delete_current()?; + } + } + + for (storage_key, (old_storage_value, _new_storage_value)) in storage { + let hashed_address = keccak256(address); + let storage_entry = + StorageEntry { key: *storage_key, value: *old_storage_value }; + if hashed_storage_cursor + .seek_by_key_subkey(hashed_address, *storage_key)? + .filter(|s| s.key == *storage_key) + .is_some() + { + hashed_storage_cursor.delete_current()? + } + + if !old_storage_value.is_zero() { + hashed_storage_cursor.upsert(hashed_address, &storage_entry)?; + } } } + } else { + // This is not working for blocks that are not at tip. as plain state is not the last + // state of end range. We should rename the functions or add support to access + // History state. Accessing history state can be tricky but we are not gaining + // anything. + let mut plain_accounts_cursor = self.tx.cursor_write::()?; + let mut plain_storage_cursor = + self.tx.cursor_dup_write::()?; - // revert storages - for (storage_key, (old_storage_value, _new_storage_value)) in storage { - let storage_entry = StorageEntry { key: *storage_key, value: *old_storage_value }; - // delete previous value - if plain_storage_cursor - .seek_by_key_subkey(*address, *storage_key)? - .filter(|s| s.key == *storage_key) - .is_some() - { - plain_storage_cursor.delete_current()? + let (state, _) = self.populate_bundle_state( + account_changeset, + storage_changeset, + &mut plain_accounts_cursor, + &mut plain_storage_cursor, + )?; + + for (address, (old_account, new_account, storage)) in &state { + if old_account != new_account { + let existing_entry = plain_accounts_cursor.seek_exact(*address)?; + if let Some(account) = old_account { + plain_accounts_cursor.upsert(*address, account)?; + } else if existing_entry.is_some() { + plain_accounts_cursor.delete_current()?; + } } - // insert value if needed - if !old_storage_value.is_zero() { - plain_storage_cursor.upsert(*address, &storage_entry)?; + for (storage_key, (old_storage_value, _new_storage_value)) in storage { + let storage_entry = + StorageEntry { key: *storage_key, value: *old_storage_value }; + if plain_storage_cursor + .seek_by_key_subkey(*address, *storage_key)? + .filter(|s| s.key == *storage_key) + .is_some() + { + plain_storage_cursor.delete_current()? + } + + if !old_storage_value.is_zero() { + plain_storage_cursor.upsert(*address, &storage_entry)?; + } } } } @@ -2669,15 +2877,13 @@ impl StateWriter changesets } else { self.take::(storage_range)? + .into_iter() + .map(|(k, v)| { + (k, ChangesetEntry { key: StorageSlotKey::plain(v.key), value: v.value }) + }) + .collect() }; - // This is not working for blocks that are not at tip. as plain state is not the last - // state of end range. We should rename the functions or add support to access - // History state. Accessing history state can be tricky but we are not gaining - // anything. - let mut plain_accounts_cursor = self.tx.cursor_write::()?; - let mut plain_storage_cursor = self.tx.cursor_dup_write::()?; - // if there are static files for this segment, prune them. let highest_changeset_block = self .static_file_provider @@ -2698,45 +2904,92 @@ impl StateWriter self.take::(range)? }; - // populate bundle state and reverts from changesets / state cursors, to iterate over, - // remove, and return later - let (state, reverts) = self.populate_bundle_state( - account_changeset, - storage_changeset, - &mut plain_accounts_cursor, - &mut plain_storage_cursor, - )?; + let (state, reverts) = if self.cached_storage_settings().use_hashed_state { + let mut hashed_accounts_cursor = self.tx.cursor_write::()?; + let mut hashed_storage_cursor = self.tx.cursor_dup_write::()?; - // iterate over local plain state remove all account and all storages. - for (address, (old_account, new_account, storage)) in &state { - // revert account if needed. - if old_account != new_account { - let existing_entry = plain_accounts_cursor.seek_exact(*address)?; - if let Some(account) = old_account { - plain_accounts_cursor.upsert(*address, account)?; - } else if existing_entry.is_some() { - plain_accounts_cursor.delete_current()?; + let (state, reverts) = self.populate_bundle_state_hashed( + account_changeset, + storage_changeset, + &mut hashed_accounts_cursor, + &mut hashed_storage_cursor, + )?; + + for (address, (old_account, new_account, storage)) in &state { + if old_account != new_account { + let hashed_address = keccak256(address); + let existing_entry = hashed_accounts_cursor.seek_exact(hashed_address)?; + if let Some(account) = old_account { + hashed_accounts_cursor.upsert(hashed_address, account)?; + } else if existing_entry.is_some() { + hashed_accounts_cursor.delete_current()?; + } + } + + for (storage_key, (old_storage_value, _new_storage_value)) in storage { + let hashed_address = keccak256(address); + let storage_entry = + StorageEntry { key: *storage_key, value: *old_storage_value }; + if hashed_storage_cursor + .seek_by_key_subkey(hashed_address, *storage_key)? + .filter(|s| s.key == *storage_key) + .is_some() + { + hashed_storage_cursor.delete_current()? + } + + if !old_storage_value.is_zero() { + hashed_storage_cursor.upsert(hashed_address, &storage_entry)?; + } } } - // revert storages - for (storage_key, (old_storage_value, _new_storage_value)) in storage { - let storage_entry = StorageEntry { key: *storage_key, value: *old_storage_value }; - // delete previous value - if plain_storage_cursor - .seek_by_key_subkey(*address, *storage_key)? - .filter(|s| s.key == *storage_key) - .is_some() - { - plain_storage_cursor.delete_current()? + (state, reverts) + } else { + // This is not working for blocks that are not at tip. as plain state is not the last + // state of end range. We should rename the functions or add support to access + // History state. Accessing history state can be tricky but we are not gaining + // anything. + let mut plain_accounts_cursor = self.tx.cursor_write::()?; + let mut plain_storage_cursor = + self.tx.cursor_dup_write::()?; + + let (state, reverts) = self.populate_bundle_state( + account_changeset, + storage_changeset, + &mut plain_accounts_cursor, + &mut plain_storage_cursor, + )?; + + for (address, (old_account, new_account, storage)) in &state { + if old_account != new_account { + let existing_entry = plain_accounts_cursor.seek_exact(*address)?; + if let Some(account) = old_account { + plain_accounts_cursor.upsert(*address, account)?; + } else if existing_entry.is_some() { + plain_accounts_cursor.delete_current()?; + } } - // insert value if needed - if !old_storage_value.is_zero() { - plain_storage_cursor.upsert(*address, &storage_entry)?; + for (storage_key, (old_storage_value, _new_storage_value)) in storage { + let storage_entry = + StorageEntry { key: *storage_key, value: *old_storage_value }; + if plain_storage_cursor + .seek_by_key_subkey(*address, *storage_key)? + .filter(|s| s.key == *storage_key) + .is_some() + { + plain_storage_cursor.delete_current()? + } + + if !old_storage_value.is_zero() { + plain_storage_cursor.upsert(*address, &storage_entry)?; + } } } - } + + (state, reverts) + }; // Collect receipts into tuples (tx_num, receipt) to correctly handle pruned receipts let mut receipts_iter = self @@ -2910,13 +3163,14 @@ impl HashingWriter for DatabaseProvi fn unwind_storage_hashing( &self, - changesets: impl Iterator, + changesets: impl Iterator, ) -> ProviderResult>> { // Aggregate all block changesets and make list of accounts that have been changed. let mut hashed_storages = changesets .into_iter() .map(|(BlockNumberAddress((_, address)), storage_entry)| { - (keccak256(address), keccak256(storage_entry.key), storage_entry.value) + let hashed_key = storage_entry.key.to_hashed(); + (keccak256(address), hashed_key, storage_entry.value) }) .collect::>(); hashed_storages.sort_by_key(|(ha, hk, _)| (*ha, *hk)); @@ -3058,11 +3312,13 @@ impl HistoryWriter for DatabaseProvi fn unwind_storage_history_indices( &self, - changesets: impl Iterator, + changesets: impl Iterator, ) -> ProviderResult { let mut storage_changesets = changesets .into_iter() - .map(|(BlockNumberAddress((bn, address)), storage)| (address, storage.key, bn)) + .map(|(BlockNumberAddress((bn, address)), storage)| { + (address, storage.key.as_b256(), bn) + }) .collect::>(); storage_changesets.sort_by_key(|(address, key, _)| (*address, *key)); @@ -3389,6 +3645,7 @@ impl BlockWriter // This is necessary because with edge storage, changesets are written to static files // whose index isn't updated until commit, making them invisible to subsequent reads // within the same transaction. + let use_hashed = self.cached_storage_settings().use_hashed_state; let (account_transitions, storage_transitions) = { let mut account_transitions: BTreeMap> = BTreeMap::new(); let mut storage_transitions: BTreeMap<(Address, B256), Vec> = BTreeMap::new(); @@ -3397,7 +3654,8 @@ impl BlockWriter for (address, account_revert) in block_reverts { account_transitions.entry(*address).or_default().push(block_number); for storage_key in account_revert.storage.keys() { - let key = B256::new(storage_key.to_be_bytes()); + let key = + StorageSlotKey::from_u256(*storage_key).to_changeset_key(use_hashed); storage_transitions.entry((*address, key)).or_default().push(block_number); } } @@ -3639,10 +3897,19 @@ mod tests { test_utils::{blocks::BlockchainTestData, create_test_provider_factory}, BlockWriter, }; - use alloy_primitives::map::B256Map; + use alloy_consensus::Header; + use alloy_primitives::{ + map::{AddressMap, B256Map}, + U256, + }; + use reth_chain_state::ExecutedBlock; use reth_ethereum_primitives::Receipt; + use reth_execution_types::{AccountRevertInit, BlockExecutionOutput, BlockExecutionResult}; + use reth_primitives_traits::SealedBlock; use reth_testing_utils::generators::{self, random_block, BlockParams}; - use reth_trie::{Nibbles, StoredNibblesSubKey}; + use reth_trie::{HashedPostState, KeccakKeyHasher, Nibbles, StoredNibblesSubKey}; + use revm_database::BundleState; + use revm_state::AccountInfo; #[test] fn test_receipts_by_block_range_empty_range() { @@ -4283,4 +4550,935 @@ mod tests { Ok(_) => panic!("Expected error, got Ok"), } } + + #[test] + fn test_unwind_storage_hashing_with_hashed_state() { + let factory = create_test_provider_factory(); + let storage_settings = StorageSettings::v2(); + factory.set_storage_settings_cache(storage_settings); + + let address = Address::random(); + let hashed_address = keccak256(address); + + let slot_key_already_hashed = B256::random(); + + let current_value = U256::from(100); + let old_value = U256::from(42); + + let provider_rw = factory.provider_rw().unwrap(); + provider_rw + .tx + .cursor_dup_write::() + .unwrap() + .upsert( + hashed_address, + &StorageEntry { key: slot_key_already_hashed, value: current_value }, + ) + .unwrap(); + + let changesets = vec![( + BlockNumberAddress((1, address)), + ChangesetEntry { + key: StorageSlotKey::Hashed(slot_key_already_hashed), + value: old_value, + }, + )]; + + let result = provider_rw.unwind_storage_hashing(changesets.into_iter()).unwrap(); + + assert_eq!(result.len(), 1); + assert!(result.contains_key(&hashed_address)); + assert!(result[&hashed_address].contains(&slot_key_already_hashed)); + + let mut cursor = provider_rw.tx.cursor_dup_read::().unwrap(); + let entry = cursor + .seek_by_key_subkey(hashed_address, slot_key_already_hashed) + .unwrap() + .expect("entry should exist"); + assert_eq!(entry.key, slot_key_already_hashed); + assert_eq!(entry.value, old_value); + } + + #[test] + fn test_write_and_remove_state_roundtrip_legacy() { + let factory = create_test_provider_factory(); + let storage_settings = StorageSettings::v1(); + assert!(!storage_settings.use_hashed_state); + factory.set_storage_settings_cache(storage_settings); + + let address = Address::with_last_byte(1); + let hashed_address = keccak256(address); + let slot = U256::from(5); + let slot_key = B256::from(slot); + let hashed_slot = keccak256(slot_key); + + let mut rng = generators::rng(); + let block0 = + random_block(&mut rng, 0, BlockParams { tx_count: Some(0), ..Default::default() }); + let block1 = + random_block(&mut rng, 1, BlockParams { tx_count: Some(0), ..Default::default() }); + + { + let provider_rw = factory.provider_rw().unwrap(); + provider_rw.insert_block(&block0.try_recover().unwrap()).unwrap(); + provider_rw.insert_block(&block1.try_recover().unwrap()).unwrap(); + provider_rw + .tx + .cursor_write::() + .unwrap() + .upsert(address, &Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }) + .unwrap(); + provider_rw.commit().unwrap(); + } + + let provider_rw = factory.provider_rw().unwrap(); + + let mut state_init: BundleStateInit = AddressMap::default(); + let mut storage_map: B256Map<(U256, U256)> = B256Map::default(); + storage_map.insert(slot_key, (U256::ZERO, U256::from(10))); + state_init.insert( + address, + ( + Some(Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }), + Some(Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None }), + storage_map, + ), + ); + + let mut reverts_init: RevertsInit = HashMap::default(); + let mut block_reverts: AddressMap = AddressMap::default(); + block_reverts.insert( + address, + ( + Some(Some(Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None })), + vec![StorageEntry { key: slot_key, value: U256::ZERO }], + ), + ); + reverts_init.insert(1, block_reverts); + + let execution_outcome = + ExecutionOutcome::new_init(state_init, reverts_init, [], vec![vec![]], 1, vec![]); + + provider_rw + .write_state( + &execution_outcome, + OriginalValuesKnown::Yes, + StateWriteConfig { + write_receipts: false, + write_account_changesets: true, + write_storage_changesets: true, + }, + ) + .unwrap(); + + let hashed_state = + execution_outcome.hash_state_slow::().into_sorted(); + provider_rw.write_hashed_state(&hashed_state).unwrap(); + + let account = provider_rw + .tx + .cursor_read::() + .unwrap() + .seek_exact(address) + .unwrap() + .unwrap() + .1; + assert_eq!(account.nonce, 1); + + let storage_entry = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(address, slot_key) + .unwrap() + .unwrap(); + assert_eq!(storage_entry.key, slot_key); + assert_eq!(storage_entry.value, U256::from(10)); + + let hashed_entry = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(hashed_address, hashed_slot) + .unwrap() + .unwrap(); + assert_eq!(hashed_entry.key, hashed_slot); + assert_eq!(hashed_entry.value, U256::from(10)); + + let account_cs_entries = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .walk(Some(1)) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(!account_cs_entries.is_empty()); + + let storage_cs_entries = provider_rw + .tx + .cursor_read::() + .unwrap() + .walk(Some(BlockNumberAddress((1, address)))) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(!storage_cs_entries.is_empty()); + assert_eq!(storage_cs_entries[0].1.key, slot_key); + + provider_rw.remove_state_above(0).unwrap(); + + let restored_account = provider_rw + .tx + .cursor_read::() + .unwrap() + .seek_exact(address) + .unwrap() + .unwrap() + .1; + assert_eq!(restored_account.nonce, 0); + + let storage_gone = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(address, slot_key) + .unwrap(); + assert!(storage_gone.is_none() || storage_gone.unwrap().key != slot_key); + + let account_cs_after = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .walk(Some(1)) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(account_cs_after.is_empty()); + + let storage_cs_after = provider_rw + .tx + .cursor_read::() + .unwrap() + .walk(Some(BlockNumberAddress((1, address)))) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(storage_cs_after.is_empty()); + } + + #[test] + fn test_unwind_storage_hashing_legacy() { + let factory = create_test_provider_factory(); + let storage_settings = StorageSettings::v1(); + assert!(!storage_settings.use_hashed_state); + factory.set_storage_settings_cache(storage_settings); + + let address = Address::random(); + let hashed_address = keccak256(address); + + let plain_slot = B256::random(); + let hashed_slot = keccak256(plain_slot); + + let current_value = U256::from(100); + let old_value = U256::from(42); + + let provider_rw = factory.provider_rw().unwrap(); + provider_rw + .tx + .cursor_dup_write::() + .unwrap() + .upsert(hashed_address, &StorageEntry { key: hashed_slot, value: current_value }) + .unwrap(); + + let changesets = vec![( + BlockNumberAddress((1, address)), + ChangesetEntry { key: StorageSlotKey::Plain(plain_slot), value: old_value }, + )]; + + let result = provider_rw.unwind_storage_hashing(changesets.into_iter()).unwrap(); + + assert_eq!(result.len(), 1); + assert!(result.contains_key(&hashed_address)); + assert!(result[&hashed_address].contains(&hashed_slot)); + + let mut cursor = provider_rw.tx.cursor_dup_read::().unwrap(); + let entry = cursor + .seek_by_key_subkey(hashed_address, hashed_slot) + .unwrap() + .expect("entry should exist"); + assert_eq!(entry.key, hashed_slot); + assert_eq!(entry.value, old_value); + } + + #[test] + fn test_write_state_and_historical_read_hashed() { + use reth_storage_api::StateProvider; + use reth_trie::{HashedPostState, KeccakKeyHasher}; + use revm_database::BundleState; + use revm_state::AccountInfo; + + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let address = Address::with_last_byte(1); + let slot = U256::from(5); + let slot_key = B256::from(slot); + let hashed_address = keccak256(address); + let hashed_slot = keccak256(slot_key); + + { + let sf = factory.static_file_provider(); + let mut hw = sf.latest_writer(StaticFileSegment::Headers).unwrap(); + let h0 = alloy_consensus::Header { number: 0, ..Default::default() }; + hw.append_header(&h0, &B256::ZERO).unwrap(); + let h1 = alloy_consensus::Header { number: 1, ..Default::default() }; + hw.append_header(&h1, &B256::ZERO).unwrap(); + hw.commit().unwrap(); + + let mut aw = sf.latest_writer(StaticFileSegment::AccountChangeSets).unwrap(); + aw.append_account_changeset(vec![], 0).unwrap(); + aw.commit().unwrap(); + + let mut sw = sf.latest_writer(StaticFileSegment::StorageChangeSets).unwrap(); + sw.append_storage_changeset(vec![], 0).unwrap(); + sw.commit().unwrap(); + } + + let provider_rw = factory.provider_rw().unwrap(); + + let bundle = BundleState::builder(1..=1) + .state_present_account_info( + address, + AccountInfo { nonce: 1, balance: U256::from(10), ..Default::default() }, + ) + .state_storage(address, HashMap::from_iter([(slot, (U256::ZERO, U256::from(10)))])) + .revert_account_info(1, address, Some(None)) + .revert_storage(1, address, vec![(slot, U256::ZERO)]) + .build(); + + let execution_outcome = ExecutionOutcome::new(bundle.clone(), vec![vec![]], 1, Vec::new()); + + provider_rw + .tx + .put::( + 1, + StoredBlockBodyIndices { first_tx_num: 0, tx_count: 0 }, + ) + .unwrap(); + + provider_rw + .write_state( + &execution_outcome, + OriginalValuesKnown::Yes, + StateWriteConfig { + write_receipts: false, + write_account_changesets: true, + write_storage_changesets: true, + }, + ) + .unwrap(); + + let hashed_state = + HashedPostState::from_bundle_state::(bundle.state()).into_sorted(); + provider_rw.write_hashed_state(&hashed_state).unwrap(); + + let plain_storage_entries = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .walk(None) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!(plain_storage_entries.is_empty()); + + let hashed_entry = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(hashed_address, hashed_slot) + .unwrap() + .unwrap(); + assert_eq!(hashed_entry.key, hashed_slot); + assert_eq!(hashed_entry.value, U256::from(10)); + + provider_rw.static_file_provider().commit().unwrap(); + + let sf = factory.static_file_provider(); + let storage_cs = sf.storage_changeset(1).unwrap(); + assert!(!storage_cs.is_empty()); + assert_eq!(storage_cs[0].1.key.as_b256(), hashed_slot); + + let account_cs = sf.account_block_changeset(1).unwrap(); + assert!(!account_cs.is_empty()); + assert_eq!(account_cs[0].address, address); + + let historical_value = + HistoricalStateProviderRef::new(&*provider_rw, 0).storage(address, slot_key).unwrap(); + assert_eq!(historical_value, None); + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum StorageMode { + V1, + V2, + } + + fn run_save_blocks_and_verify(mode: StorageMode) { + use alloy_primitives::map::HashMap; + + let factory = create_test_provider_factory(); + + match mode { + StorageMode::V1 => factory.set_storage_settings_cache(StorageSettings::v1()), + StorageMode::V2 => factory.set_storage_settings_cache(StorageSettings::v2()), + } + + let num_blocks = 3u64; + let accounts_per_block = 5usize; + let slots_per_account = 3usize; + + let genesis = SealedBlock::::from_sealed_parts( + SealedHeader::new( + Header { number: 0, difficulty: U256::from(1), ..Default::default() }, + B256::ZERO, + ), + Default::default(), + ); + + let genesis_executed = ExecutedBlock::new( + Arc::new(genesis.try_recover().unwrap()), + Arc::new(BlockExecutionOutput { + result: BlockExecutionResult { + receipts: vec![], + requests: Default::default(), + gas_used: 0, + blob_gas_used: 0, + }, + state: Default::default(), + }), + ComputedTrieData::default(), + ); + let provider_rw = factory.provider_rw().unwrap(); + provider_rw.save_blocks(vec![genesis_executed], SaveBlocksMode::Full).unwrap(); + provider_rw.commit().unwrap(); + + let mut blocks: Vec = Vec::new(); + let mut parent_hash = B256::ZERO; + + for block_num in 1..=num_blocks { + let mut builder = BundleState::builder(block_num..=block_num); + + for acct_idx in 0..accounts_per_block { + let address = Address::with_last_byte((block_num * 10 + acct_idx as u64) as u8); + let info = AccountInfo { + nonce: block_num, + balance: U256::from(block_num * 100 + acct_idx as u64), + ..Default::default() + }; + + let storage: HashMap = (1..=slots_per_account as u64) + .map(|s| { + ( + U256::from(s + acct_idx as u64 * 100), + (U256::ZERO, U256::from(block_num * 1000 + s)), + ) + }) + .collect(); + + let revert_storage: Vec<(U256, U256)> = (1..=slots_per_account as u64) + .map(|s| (U256::from(s + acct_idx as u64 * 100), U256::ZERO)) + .collect(); + + builder = builder + .state_present_account_info(address, info) + .revert_account_info(block_num, address, Some(None)) + .state_storage(address, storage) + .revert_storage(block_num, address, revert_storage); + } + + let bundle = builder.build(); + + let hashed_state = + HashedPostState::from_bundle_state::(bundle.state()).into_sorted(); + + let header = Header { + number: block_num, + parent_hash, + difficulty: U256::from(1), + ..Default::default() + }; + let block = SealedBlock::::seal_parts( + header, + Default::default(), + ); + parent_hash = block.hash(); + + let executed = ExecutedBlock::new( + Arc::new(block.try_recover().unwrap()), + Arc::new(BlockExecutionOutput { + result: BlockExecutionResult { + receipts: vec![], + requests: Default::default(), + gas_used: 0, + blob_gas_used: 0, + }, + state: bundle, + }), + ComputedTrieData { hashed_state: Arc::new(hashed_state), ..Default::default() }, + ); + blocks.push(executed); + } + + let provider_rw = factory.provider_rw().unwrap(); + provider_rw.save_blocks(blocks, SaveBlocksMode::Full).unwrap(); + provider_rw.commit().unwrap(); + + let provider = factory.provider().unwrap(); + + for block_num in 1..=num_blocks { + for acct_idx in 0..accounts_per_block { + let address = Address::with_last_byte((block_num * 10 + acct_idx as u64) as u8); + let hashed_address = keccak256(address); + + let ha_entry = provider + .tx_ref() + .cursor_read::() + .unwrap() + .seek_exact(hashed_address) + .unwrap(); + assert!( + ha_entry.is_some(), + "HashedAccounts missing for block {block_num} acct {acct_idx}" + ); + + for s in 1..=slots_per_account as u64 { + let slot = U256::from(s + acct_idx as u64 * 100); + let slot_key = B256::from(slot); + let hashed_slot = keccak256(slot_key); + + let hs_entry = provider + .tx_ref() + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(hashed_address, hashed_slot) + .unwrap(); + assert!( + hs_entry.is_some(), + "HashedStorages missing for block {block_num} acct {acct_idx} slot {s}" + ); + let entry = hs_entry.unwrap(); + assert_eq!(entry.key, hashed_slot); + assert_eq!(entry.value, U256::from(block_num * 1000 + s)); + } + } + } + + for block_num in 1..=num_blocks { + let header = provider.header_by_number(block_num).unwrap(); + assert!(header.is_some(), "Header missing for block {block_num}"); + + let indices = provider.block_body_indices(block_num).unwrap(); + assert!(indices.is_some(), "BlockBodyIndices missing for block {block_num}"); + } + + let plain_accounts = provider.tx_ref().entries::().unwrap(); + let plain_storage = provider.tx_ref().entries::().unwrap(); + + if mode == StorageMode::V2 { + assert_eq!(plain_accounts, 0, "v2: PlainAccountState should be empty"); + assert_eq!(plain_storage, 0, "v2: PlainStorageState should be empty"); + + let mdbx_account_cs = provider.tx_ref().entries::().unwrap(); + assert_eq!(mdbx_account_cs, 0, "v2: AccountChangeSets in MDBX should be empty"); + + let mdbx_storage_cs = provider.tx_ref().entries::().unwrap(); + assert_eq!(mdbx_storage_cs, 0, "v2: StorageChangeSets in MDBX should be empty"); + + provider.static_file_provider().commit().unwrap(); + let sf = factory.static_file_provider(); + + for block_num in 1..=num_blocks { + let account_cs = sf.account_block_changeset(block_num).unwrap(); + assert!( + !account_cs.is_empty(), + "v2: static file AccountChangeSets should exist for block {block_num}" + ); + + let storage_cs = sf.storage_changeset(block_num).unwrap(); + assert!( + !storage_cs.is_empty(), + "v2: static file StorageChangeSets should exist for block {block_num}" + ); + + for (_, entry) in &storage_cs { + assert!( + entry.key.is_hashed(), + "v2: static file storage changeset should have hashed slot keys" + ); + } + } + + #[cfg(all(unix, feature = "rocksdb"))] + { + let rocksdb = factory.rocksdb_provider(); + for block_num in 1..=num_blocks { + for acct_idx in 0..accounts_per_block { + let address = + Address::with_last_byte((block_num * 10 + acct_idx as u64) as u8); + let shards = rocksdb.account_history_shards(address).unwrap(); + assert!( + !shards.is_empty(), + "v2: RocksDB AccountsHistory missing for block {block_num} acct {acct_idx}" + ); + + for s in 1..=slots_per_account as u64 { + let slot = U256::from(s + acct_idx as u64 * 100); + let slot_key = B256::from(slot); + let hashed_slot = keccak256(slot_key); + + let shards = + rocksdb.storage_history_shards(address, hashed_slot).unwrap(); + assert!( + !shards.is_empty(), + "v2: RocksDB StoragesHistory missing for block {block_num} acct {acct_idx} slot {s}" + ); + } + } + } + } + } else { + assert!(plain_accounts > 0, "v1: PlainAccountState should not be empty"); + assert!(plain_storage > 0, "v1: PlainStorageState should not be empty"); + + let mdbx_account_cs = provider.tx_ref().entries::().unwrap(); + assert!(mdbx_account_cs > 0, "v1: AccountChangeSets in MDBX should not be empty"); + + let mdbx_storage_cs = provider.tx_ref().entries::().unwrap(); + assert!(mdbx_storage_cs > 0, "v1: StorageChangeSets in MDBX should not be empty"); + + for block_num in 1..=num_blocks { + let storage_entries: Vec<_> = provider + .tx_ref() + .cursor_dup_read::() + .unwrap() + .walk_range(BlockNumberAddress::range(block_num..=block_num)) + .unwrap() + .collect::, _>>() + .unwrap(); + assert!( + !storage_entries.is_empty(), + "v1: MDBX StorageChangeSets should have entries for block {block_num}" + ); + + for (_, entry) in &storage_entries { + let slot_key = B256::from(entry.key); + assert!( + slot_key != keccak256(slot_key), + "v1: storage changeset keys should be plain (not hashed)" + ); + } + } + + let mdbx_account_history = + provider.tx_ref().entries::().unwrap(); + assert!(mdbx_account_history > 0, "v1: AccountsHistory in MDBX should not be empty"); + + let mdbx_storage_history = + provider.tx_ref().entries::().unwrap(); + assert!(mdbx_storage_history > 0, "v1: StoragesHistory in MDBX should not be empty"); + } + } + + #[test] + fn test_save_blocks_v1_table_assertions() { + run_save_blocks_and_verify(StorageMode::V1); + } + + #[test] + fn test_save_blocks_v2_table_assertions() { + run_save_blocks_and_verify(StorageMode::V2); + } + + #[test] + fn test_write_and_remove_state_roundtrip_v2() { + let factory = create_test_provider_factory(); + let storage_settings = StorageSettings::v2(); + assert!(storage_settings.use_hashed_state); + factory.set_storage_settings_cache(storage_settings); + + let address = Address::with_last_byte(1); + let hashed_address = keccak256(address); + let slot = U256::from(5); + let slot_key = B256::from(slot); + let hashed_slot = keccak256(slot_key); + + { + let sf = factory.static_file_provider(); + let mut hw = sf.latest_writer(StaticFileSegment::Headers).unwrap(); + let h0 = alloy_consensus::Header { number: 0, ..Default::default() }; + hw.append_header(&h0, &B256::ZERO).unwrap(); + let h1 = alloy_consensus::Header { number: 1, ..Default::default() }; + hw.append_header(&h1, &B256::ZERO).unwrap(); + hw.commit().unwrap(); + + let mut aw = sf.latest_writer(StaticFileSegment::AccountChangeSets).unwrap(); + aw.append_account_changeset(vec![], 0).unwrap(); + aw.commit().unwrap(); + + let mut sw = sf.latest_writer(StaticFileSegment::StorageChangeSets).unwrap(); + sw.append_storage_changeset(vec![], 0).unwrap(); + sw.commit().unwrap(); + } + + { + let provider_rw = factory.provider_rw().unwrap(); + provider_rw + .tx + .put::( + 0, + StoredBlockBodyIndices { first_tx_num: 0, tx_count: 0 }, + ) + .unwrap(); + provider_rw + .tx + .put::( + 1, + StoredBlockBodyIndices { first_tx_num: 0, tx_count: 0 }, + ) + .unwrap(); + provider_rw + .tx + .cursor_write::() + .unwrap() + .upsert( + hashed_address, + &Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }, + ) + .unwrap(); + provider_rw.commit().unwrap(); + } + + let provider_rw = factory.provider_rw().unwrap(); + + let bundle = BundleState::builder(1..=1) + .state_present_account_info( + address, + AccountInfo { nonce: 1, balance: U256::from(10), ..Default::default() }, + ) + .state_storage(address, HashMap::from_iter([(slot, (U256::ZERO, U256::from(10)))])) + .revert_account_info(1, address, Some(None)) + .revert_storage(1, address, vec![(slot, U256::ZERO)]) + .build(); + + let execution_outcome = ExecutionOutcome::new(bundle.clone(), vec![vec![]], 1, Vec::new()); + + provider_rw + .write_state( + &execution_outcome, + OriginalValuesKnown::Yes, + StateWriteConfig { + write_receipts: false, + write_account_changesets: true, + write_storage_changesets: true, + }, + ) + .unwrap(); + + let hashed_state = + HashedPostState::from_bundle_state::(bundle.state()).into_sorted(); + provider_rw.write_hashed_state(&hashed_state).unwrap(); + + let hashed_account = provider_rw + .tx + .cursor_read::() + .unwrap() + .seek_exact(hashed_address) + .unwrap() + .unwrap() + .1; + assert_eq!(hashed_account.nonce, 1); + + let hashed_entry = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(hashed_address, hashed_slot) + .unwrap() + .unwrap(); + assert_eq!(hashed_entry.key, hashed_slot); + assert_eq!(hashed_entry.value, U256::from(10)); + + let plain_accounts = provider_rw.tx.entries::().unwrap(); + assert_eq!(plain_accounts, 0, "v2: PlainAccountState should be empty"); + + let plain_storage = provider_rw.tx.entries::().unwrap(); + assert_eq!(plain_storage, 0, "v2: PlainStorageState should be empty"); + + provider_rw.static_file_provider().commit().unwrap(); + + let sf = factory.static_file_provider(); + let storage_cs = sf.storage_changeset(1).unwrap(); + assert!(!storage_cs.is_empty(), "v2: storage changesets should be in static files"); + assert_eq!( + storage_cs[0].1.key.as_b256(), + hashed_slot, + "v2: changeset key should be hashed" + ); + + provider_rw.remove_state_above(0).unwrap(); + + let restored_account = provider_rw + .tx + .cursor_read::() + .unwrap() + .seek_exact(hashed_address) + .unwrap(); + assert!( + restored_account.is_none(), + "v2: account should be removed (didn't exist before block 1)" + ); + + let storage_gone = provider_rw + .tx + .cursor_dup_read::() + .unwrap() + .seek_by_key_subkey(hashed_address, hashed_slot) + .unwrap(); + assert!( + storage_gone.is_none() || storage_gone.unwrap().key != hashed_slot, + "v2: storage should be reverted (removed or different key)" + ); + + let mdbx_storage_cs = provider_rw.tx.entries::().unwrap(); + assert_eq!(mdbx_storage_cs, 0, "v2: MDBX StorageChangeSets should remain empty"); + + let mdbx_account_cs = provider_rw.tx.entries::().unwrap(); + assert_eq!(mdbx_account_cs, 0, "v2: MDBX AccountChangeSets should remain empty"); + } + + #[test] + fn test_populate_bundle_state_hashed_with_hashed_keys() { + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let address = Address::with_last_byte(1); + let hashed_address = keccak256(address); + let slot_key = B256::from(U256::from(42)); + let hashed_slot = keccak256(slot_key); + let current_value = U256::from(100); + let old_value = U256::from(50); + + let provider_rw = factory.provider_rw().unwrap(); + + provider_rw + .tx + .cursor_write::() + .unwrap() + .upsert(hashed_address, &Account { nonce: 1, balance: U256::ZERO, bytecode_hash: None }) + .unwrap(); + provider_rw + .tx + .cursor_dup_write::() + .unwrap() + .upsert(hashed_address, &StorageEntry { key: hashed_slot, value: current_value }) + .unwrap(); + + let storage_changeset = vec![( + BlockNumberAddress((1, address)), + ChangesetEntry { key: StorageSlotKey::Hashed(hashed_slot), value: old_value }, + )]; + + let account_changeset = vec![( + 1u64, + AccountBeforeTx { + address, + info: Some(Account { nonce: 0, balance: U256::ZERO, bytecode_hash: None }), + }, + )]; + + let mut hashed_accounts_cursor = + provider_rw.tx.cursor_read::().unwrap(); + let mut hashed_storage_cursor = + provider_rw.tx.cursor_dup_read::().unwrap(); + + let (state, reverts) = provider_rw + .populate_bundle_state_hashed( + account_changeset, + storage_changeset, + &mut hashed_accounts_cursor, + &mut hashed_storage_cursor, + ) + .unwrap(); + + let (_, new_account, storage_map) = + state.get(&address).expect("address should be in state"); + assert!(new_account.is_some()); + assert_eq!(new_account.unwrap().nonce, 1); + + let (old_val, new_val) = + storage_map.get(&hashed_slot).expect("hashed slot should be in storage map"); + assert_eq!(*old_val, old_value); + assert_eq!(*new_val, current_value); + + let block_reverts = reverts.get(&1).expect("block 1 should have reverts"); + let (_, storage_reverts) = + block_reverts.get(&address).expect("address should have reverts"); + assert_eq!(storage_reverts.len(), 1); + assert_eq!(storage_reverts[0].key, hashed_slot); + assert_eq!(storage_reverts[0].value, old_value); + } + + #[test] + #[cfg(all(unix, feature = "rocksdb"))] + fn test_unwind_storage_history_indices_v2() { + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let address = Address::with_last_byte(1); + let slot_key = B256::from(U256::from(42)); + let hashed_slot = keccak256(slot_key); + + { + let rocksdb = factory.rocksdb_provider(); + let mut batch = rocksdb.batch(); + batch.append_storage_history_shard(address, hashed_slot, vec![3u64, 7, 10]).unwrap(); + batch.commit().unwrap(); + + let shards = rocksdb.storage_history_shards(address, hashed_slot).unwrap(); + assert!(!shards.is_empty(), "history should be written to rocksdb"); + } + + let provider_rw = factory.provider_rw().unwrap(); + + let changesets = vec![ + ( + BlockNumberAddress((7, address)), + ChangesetEntry { key: StorageSlotKey::Hashed(hashed_slot), value: U256::from(5) }, + ), + ( + BlockNumberAddress((10, address)), + ChangesetEntry { key: StorageSlotKey::Hashed(hashed_slot), value: U256::from(8) }, + ), + ]; + + let count = provider_rw.unwind_storage_history_indices(changesets.into_iter()).unwrap(); + assert_eq!(count, 2); + + provider_rw.commit().unwrap(); + + let rocksdb = factory.rocksdb_provider(); + let shards = rocksdb.storage_history_shards(address, hashed_slot).unwrap(); + + assert!( + !shards.is_empty(), + "history shards should still exist with block 3 after partial unwind" + ); + + let all_blocks: Vec = shards.iter().flat_map(|(_, list)| list.iter()).collect(); + assert!(all_blocks.contains(&3), "block 3 should remain"); + assert!(!all_blocks.contains(&7), "block 7 should be unwound"); + assert!(!all_blocks.contains(&10), "block 10 should be unwound"); + } } diff --git a/crates/storage/provider/src/providers/rocksdb/invariants.rs b/crates/storage/provider/src/providers/rocksdb/invariants.rs index 6c73170562..1622494241 100644 --- a/crates/storage/provider/src/providers/rocksdb/invariants.rs +++ b/crates/storage/provider/src/providers/rocksdb/invariants.rs @@ -317,7 +317,10 @@ impl RocksDBProvider { let unique_keys: HashSet<_> = changesets .into_iter() - .map(|(block_addr, entry)| (block_addr.address(), entry.key, checkpoint + 1)) + .map(|(block_addr, entry)| { + // entry.key is a hashed storage key + (block_addr.address(), entry.key.as_b256(), checkpoint + 1) + }) .collect(); let indices: Vec<_> = unique_keys.into_iter().collect(); diff --git a/crates/storage/provider/src/providers/rocksdb/provider.rs b/crates/storage/provider/src/providers/rocksdb/provider.rs index 77b37e410c..7dee3fd297 100644 --- a/crates/storage/provider/src/providers/rocksdb/provider.rs +++ b/crates/storage/provider/src/providers/rocksdb/provider.rs @@ -2,6 +2,7 @@ use super::metrics::{RocksDBMetrics, RocksDBOperation, ROCKSDB_TABLES}; use crate::providers::{compute_history_rank, needs_prev_shard_check, HistoryInfo}; use alloy_consensus::transaction::TxHashRef; use alloy_primitives::{ + keccak256, map::{AddressMap, HashMap}, Address, BlockNumber, TxNumber, B256, }; @@ -1336,7 +1337,8 @@ impl RocksDBProvider { for storage_block_reverts in reverts.storage { for revert in storage_block_reverts { for (slot, _) in revert.storage_revert { - let key = B256::new(slot.to_be_bytes()); + let plain_key = B256::new(slot.to_be_bytes()); + let key = keccak256(plain_key); storage_history .entry((revert.address, key)) .or_default() diff --git a/crates/storage/provider/src/providers/state/historical.rs b/crates/storage/provider/src/providers/state/historical.rs index ed0b53b80b..8d1b8068a2 100644 --- a/crates/storage/provider/src/providers/state/historical.rs +++ b/crates/storage/provider/src/providers/state/historical.rs @@ -11,7 +11,7 @@ use reth_db_api::{ transaction::DbTx, BlockNumberList, }; -use reth_primitives_traits::{Account, Bytecode}; +use reth_primitives_traits::{Account, Bytecode, StorageSlotKey}; use reth_storage_api::{ BlockNumReader, BytecodeReader, DBProvider, NodePrimitivesProvider, StateProofProvider, StorageChangeSetReader, StorageRootProvider, StorageSettingsCache, @@ -26,8 +26,8 @@ use reth_trie::{ TrieInputSorted, }; use reth_trie_db::{ - hashed_storage_from_reverts_with_provider, DatabaseHashedPostState, DatabaseProof, - DatabaseStateRoot, DatabaseStorageProof, DatabaseStorageRoot, DatabaseTrieWitness, + hashed_storage_from_reverts_with_provider, DatabaseProof, DatabaseStateRoot, + DatabaseStorageProof, DatabaseStorageRoot, DatabaseTrieWitness, }; use std::fmt::Debug; @@ -150,7 +150,7 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block pub fn storage_history_lookup( &self, address: Address, - storage_key: StorageKey, + storage_key: StorageSlotKey, ) -> ProviderResult where Provider: StorageSettingsCache + RocksDBProviderFactory + NodePrimitivesProvider, @@ -159,17 +159,85 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block return Err(ProviderError::StateAtBlockPruned(self.block_number)) } + let lookup_key = if self.provider.cached_storage_settings().use_hashed_state { + storage_key.to_hashed() + } else { + debug_assert!( + storage_key.is_plain(), + "expected plain storage key when use_hashed_state is false" + ); + storage_key.as_b256() + }; + self.provider.with_rocksdb_tx(|rocks_tx_ref| { let mut reader = EitherReader::new_storages_history(self.provider, rocks_tx_ref)?; reader.storage_history_info( address, - storage_key, + lookup_key, self.block_number, self.lowest_available_blocks.storage_history_block_number, ) }) } + /// Resolves a storage value by looking up the given key in history, changesets, or + /// plain state. + /// + /// Accepts a [`StorageSlotKey`]; the correct lookup key is derived internally + /// based on the storage mode. + fn storage_by_lookup_key( + &self, + address: Address, + storage_key: StorageSlotKey, + ) -> ProviderResult> + where + Provider: StorageSettingsCache + RocksDBProviderFactory + NodePrimitivesProvider, + { + let lookup_key = if self.provider.cached_storage_settings().use_hashed_state { + storage_key.to_hashed() + } else { + debug_assert!( + storage_key.is_plain(), + "expected plain storage key when use_hashed_state is false" + ); + storage_key.as_b256() + }; + + match self.storage_history_lookup(address, storage_key)? { + HistoryInfo::NotYetWritten => Ok(None), + HistoryInfo::InChangeset(changeset_block_number) => self + .provider + .get_storage_before_block(changeset_block_number, address, lookup_key)? + .ok_or_else(|| ProviderError::StorageChangesetNotFound { + block_number: changeset_block_number, + address, + storage_key: Box::new(lookup_key), + }) + .map(|entry| entry.value) + .map(Some), + HistoryInfo::InPlainState | HistoryInfo::MaybeInPlainState => { + if self.provider.cached_storage_settings().use_hashed_state { + let hashed_address = alloy_primitives::keccak256(address); + Ok(self + .tx() + .cursor_dup_read::()? + .seek_by_key_subkey(hashed_address, lookup_key)? + .filter(|entry| entry.key == lookup_key) + .map(|entry| entry.value) + .or(Some(StorageValue::ZERO))) + } else { + Ok(self + .tx() + .cursor_dup_read::()? + .seek_by_key_subkey(address, lookup_key)? + .filter(|entry| entry.key == lookup_key) + .map(|entry| entry.value) + .or(Some(StorageValue::ZERO))) + } + } + } + } + /// Checks and returns `true` if distance to historical block exceeds the provided limit. fn check_distance_against_limit(&self, limit: u64) -> ProviderResult { let tip = self.provider.last_block_number()?; @@ -178,7 +246,10 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block } /// Retrieve revert hashed state for this history provider. - fn revert_state(&self) -> ProviderResult { + fn revert_state(&self) -> ProviderResult + where + Provider: StorageSettingsCache, + { if !self.lowest_available_blocks.is_account_history_available(self.block_number) || !self.lowest_available_blocks.is_storage_history_available(self.block_number) { @@ -193,11 +264,14 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block ); } - HashedPostStateSorted::from_reverts::(self.provider, self.block_number..) + reth_trie_db::from_reverts_auto(self.provider, self.block_number..) } /// Retrieve revert hashed storage for this history provider and target address. - fn revert_storage(&self, address: Address) -> ProviderResult { + fn revert_storage(&self, address: Address) -> ProviderResult + where + Provider: StorageSettingsCache, + { if !self.lowest_available_blocks.is_storage_history_available(self.block_number) { return Err(ProviderError::StateAtBlockPruned(self.block_number)) } @@ -263,7 +337,12 @@ impl< .map(|account_before| account_before.info) } HistoryInfo::InPlainState | HistoryInfo::MaybeInPlainState => { - Ok(self.tx().get_by_encoded_key::(address)?) + if self.provider.cached_storage_settings().use_hashed_state { + let hashed_address = alloy_primitives::keccak256(address); + Ok(self.tx().get_by_encoded_key::(&hashed_address)?) + } else { + Ok(self.tx().get_by_encoded_key::(address)?) + } } } } @@ -286,8 +365,13 @@ impl BlockHashReader } } -impl - StateRootProvider for HistoricalStateProviderRef<'_, Provider> +impl< + Provider: DBProvider + + ChangeSetReader + + StorageChangeSetReader + + BlockNumReader + + StorageSettingsCache, + > StateRootProvider for HistoricalStateProviderRef<'_, Provider> { fn state_root(&self, hashed_state: HashedPostState) -> ProviderResult { let mut revert_state = self.revert_state()?; @@ -323,8 +407,13 @@ impl - StorageRootProvider for HistoricalStateProviderRef<'_, Provider> +impl< + Provider: DBProvider + + ChangeSetReader + + StorageChangeSetReader + + BlockNumReader + + StorageSettingsCache, + > StorageRootProvider for HistoricalStateProviderRef<'_, Provider> { fn storage_root( &self, @@ -362,8 +451,13 @@ impl - StateProofProvider for HistoricalStateProviderRef<'_, Provider> +impl< + Provider: DBProvider + + ChangeSetReader + + StorageChangeSetReader + + BlockNumReader + + StorageSettingsCache, + > StateProofProvider for HistoricalStateProviderRef<'_, Provider> { /// Get account and storage proofs. fn proof( @@ -412,32 +506,24 @@ impl< + NodePrimitivesProvider, > StateProvider for HistoricalStateProviderRef<'_, Provider> { - /// Get storage. + /// Expects a plain (unhashed) storage key slot. fn storage( &self, address: Address, storage_key: StorageKey, ) -> ProviderResult> { - match self.storage_history_lookup(address, storage_key)? { - HistoryInfo::NotYetWritten => Ok(None), - HistoryInfo::InChangeset(changeset_block_number) => self - .provider - .get_storage_before_block(changeset_block_number, address, storage_key)? - .ok_or_else(|| ProviderError::StorageChangesetNotFound { - block_number: changeset_block_number, - address, - storage_key: Box::new(storage_key), - }) - .map(|entry| entry.value) - .map(Some), - HistoryInfo::InPlainState | HistoryInfo::MaybeInPlainState => Ok(self - .tx() - .cursor_dup_read::()? - .seek_by_key_subkey(address, storage_key)? - .filter(|entry| entry.key == storage_key) - .map(|entry| entry.value) - .or(Some(StorageValue::ZERO))), + self.storage_by_lookup_key(address, StorageSlotKey::plain(storage_key)) + } + + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: StorageKey, + ) -> ProviderResult> { + if !self.provider.cached_storage_settings().use_hashed_state { + return Err(ProviderError::UnsupportedProvider) } + self.storage_by_lookup_key(address, StorageSlotKey::hashed(hashed_storage_key)) } } @@ -630,7 +716,7 @@ mod tests { transaction::{DbTx, DbTxMut}, BlockNumberList, }; - use reth_primitives_traits::{Account, StorageEntry}; + use reth_primitives_traits::{Account, StorageEntry, StorageSlotKey}; use reth_storage_api::{ BlockHashReader, BlockNumReader, ChangeSetReader, DBProvider, DatabaseProviderFactory, NodePrimitivesProvider, StorageChangeSetReader, StorageSettingsCache, @@ -885,7 +971,7 @@ mod tests { Err(ProviderError::StateAtBlockPruned(number)) if number == provider.block_number )); assert!(matches!( - provider.storage_history_lookup(ADDRESS, STORAGE), + provider.storage_history_lookup(ADDRESS, StorageSlotKey::plain(STORAGE)), Err(ProviderError::StateAtBlockPruned(number)) if number == provider.block_number )); @@ -904,7 +990,7 @@ mod tests { Ok(HistoryInfo::MaybeInPlainState) )); assert!(matches!( - provider.storage_history_lookup(ADDRESS, STORAGE), + provider.storage_history_lookup(ADDRESS, StorageSlotKey::plain(STORAGE)), Ok(HistoryInfo::MaybeInPlainState) )); @@ -923,7 +1009,7 @@ mod tests { Ok(HistoryInfo::MaybeInPlainState) )); assert!(matches!( - provider.storage_history_lookup(ADDRESS, STORAGE), + provider.storage_history_lookup(ADDRESS, StorageSlotKey::plain(STORAGE)), Ok(HistoryInfo::MaybeInPlainState) )); } @@ -943,6 +1029,242 @@ mod tests { assert_eq!(HistoryInfo::from_lookup(None, false, None), HistoryInfo::InPlainState); } + #[test] + fn history_provider_get_storage_legacy() { + let factory = create_test_provider_factory(); + + assert!(!factory.provider().unwrap().cached_storage_settings().use_hashed_state); + + let tx = factory.provider_rw().unwrap().into_tx(); + + tx.put::( + StorageShardedKey { + address: ADDRESS, + sharded_key: ShardedKey { key: STORAGE, highest_block_number: 7 }, + }, + BlockNumberList::new([3, 7]).unwrap(), + ) + .unwrap(); + tx.put::( + StorageShardedKey { + address: ADDRESS, + sharded_key: ShardedKey { key: STORAGE, highest_block_number: u64::MAX }, + }, + BlockNumberList::new([10, 15]).unwrap(), + ) + .unwrap(); + tx.put::( + StorageShardedKey { + address: HIGHER_ADDRESS, + sharded_key: ShardedKey { key: STORAGE, highest_block_number: u64::MAX }, + }, + BlockNumberList::new([4]).unwrap(), + ) + .unwrap(); + + let higher_entry_plain = StorageEntry { key: STORAGE, value: U256::from(1000) }; + let higher_entry_at4 = StorageEntry { key: STORAGE, value: U256::from(0) }; + let entry_plain = StorageEntry { key: STORAGE, value: U256::from(100) }; + let entry_at15 = StorageEntry { key: STORAGE, value: U256::from(15) }; + let entry_at10 = StorageEntry { key: STORAGE, value: U256::from(10) }; + let entry_at7 = StorageEntry { key: STORAGE, value: U256::from(7) }; + let entry_at3 = StorageEntry { key: STORAGE, value: U256::from(0) }; + + tx.put::((3, ADDRESS).into(), entry_at3).unwrap(); + tx.put::((4, HIGHER_ADDRESS).into(), higher_entry_at4).unwrap(); + tx.put::((7, ADDRESS).into(), entry_at7).unwrap(); + tx.put::((10, ADDRESS).into(), entry_at10).unwrap(); + tx.put::((15, ADDRESS).into(), entry_at15).unwrap(); + + tx.put::(ADDRESS, entry_plain).unwrap(); + tx.put::(HIGHER_ADDRESS, higher_entry_plain).unwrap(); + tx.commit().unwrap(); + + let db = factory.provider().unwrap(); + + assert!(matches!( + HistoricalStateProviderRef::new(&db, 0).storage(ADDRESS, STORAGE), + Ok(None) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 3).storage(ADDRESS, STORAGE), + Ok(Some(U256::ZERO)) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 4).storage(ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == entry_at7.value + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 7).storage(ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == entry_at7.value + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 9).storage(ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == entry_at10.value + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 10).storage(ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == entry_at10.value + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 11).storage(ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == entry_at15.value + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 16).storage(ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == entry_plain.value + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 1).storage(HIGHER_ADDRESS, STORAGE), + Ok(None) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 1000).storage(HIGHER_ADDRESS, STORAGE), + Ok(Some(expected_value)) if expected_value == higher_entry_plain.value + )); + } + + #[test] + #[cfg(all(unix, feature = "rocksdb"))] + fn history_provider_get_storage_hashed_state() { + use crate::BlockWriter; + use alloy_primitives::keccak256; + use reth_db_api::models::StorageSettings; + use reth_execution_types::ExecutionOutcome; + use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams}; + use revm_database::BundleState; + use std::collections::HashMap; + + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let slot = U256::from_be_bytes(*STORAGE); + let account: revm_state::AccountInfo = + Account { nonce: 1, balance: U256::from(1000), bytecode_hash: None }.into(); + let higher_account: revm_state::AccountInfo = + Account { nonce: 1, balance: U256::from(2000), bytecode_hash: None }.into(); + + let mut rng = generators::rng(); + let blocks = random_block_range( + &mut rng, + 0..=15, + BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() }, + ); + + let mut addr_storage = HashMap::default(); + addr_storage.insert(slot, (U256::ZERO, U256::from(100))); + let mut higher_storage = HashMap::default(); + higher_storage.insert(slot, (U256::ZERO, U256::from(1000))); + + type Revert = Vec<(Address, Option>, Vec<(U256, U256)>)>; + let mut reverts: Vec = vec![Vec::new(); 16]; + + reverts[3] = vec![(ADDRESS, Some(Some(account.clone())), vec![(slot, U256::ZERO)])]; + reverts[4] = + vec![(HIGHER_ADDRESS, Some(Some(higher_account.clone())), vec![(slot, U256::ZERO)])]; + reverts[7] = vec![(ADDRESS, Some(Some(account.clone())), vec![(slot, U256::from(7))])]; + reverts[10] = vec![(ADDRESS, Some(Some(account.clone())), vec![(slot, U256::from(10))])]; + reverts[15] = vec![(ADDRESS, Some(Some(account.clone())), vec![(slot, U256::from(15))])]; + + let bundle = BundleState::new( + [ + (ADDRESS, None, Some(account), addr_storage), + (HIGHER_ADDRESS, None, Some(higher_account), higher_storage), + ], + reverts, + [], + ); + + let provider_rw = factory.provider_rw().unwrap(); + provider_rw + .append_blocks_with_state( + blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { bundle, first_block: 0, ..Default::default() }, + Default::default(), + ) + .unwrap(); + + let hashed_address = keccak256(ADDRESS); + let hashed_higher_address = keccak256(HIGHER_ADDRESS); + let hashed_storage = keccak256(STORAGE); + + provider_rw + .tx_ref() + .put::( + hashed_address, + StorageEntry { key: hashed_storage, value: U256::from(100) }, + ) + .unwrap(); + provider_rw + .tx_ref() + .put::( + hashed_higher_address, + StorageEntry { key: hashed_storage, value: U256::from(1000) }, + ) + .unwrap(); + provider_rw + .tx_ref() + .put::( + hashed_address, + Account { nonce: 1, balance: U256::from(1000), bytecode_hash: None }, + ) + .unwrap(); + provider_rw + .tx_ref() + .put::( + hashed_higher_address, + Account { nonce: 1, balance: U256::from(2000), bytecode_hash: None }, + ) + .unwrap(); + provider_rw.commit().unwrap(); + + let db = factory.provider().unwrap(); + + assert!(matches!( + HistoricalStateProviderRef::new(&db, 0).storage(ADDRESS, STORAGE), + Ok(None) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 3).storage(ADDRESS, STORAGE), + Ok(Some(U256::ZERO)) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 4).storage(ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(7) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 7).storage(ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(7) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 9).storage(ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(10) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 10).storage(ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(10) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 11).storage(ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(15) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 16).storage(ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(100) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 1).storage(HIGHER_ADDRESS, STORAGE), + Ok(None) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 1000).storage(HIGHER_ADDRESS, STORAGE), + Ok(Some(v)) if v == U256::from(1000) + )); + } + #[test] fn test_needs_prev_shard_check() { // Only needs check when rank == 0 and found_block != block_number @@ -951,4 +1273,105 @@ mod tests { assert!(!needs_prev_shard_check(0, Some(5), 5)); // found_block == block_number assert!(!needs_prev_shard_check(1, Some(10), 5)); // rank > 0 } + + #[test] + fn test_historical_storage_by_hashed_key_unsupported_in_v1() { + let factory = create_test_provider_factory(); + assert!(!factory.provider().unwrap().cached_storage_settings().use_hashed_state); + + let db = factory.provider().unwrap(); + let provider = HistoricalStateProviderRef::new(&db, 1); + + assert!(matches!( + provider.storage_by_hashed_key(ADDRESS, STORAGE), + Err(ProviderError::UnsupportedProvider) + )); + } + + #[test] + #[cfg(all(unix, feature = "rocksdb"))] + fn test_historical_storage_by_hashed_key_v2() { + use crate::BlockWriter; + use alloy_primitives::keccak256; + use reth_db_api::models::StorageSettings; + use reth_execution_types::ExecutionOutcome; + use reth_testing_utils::generators::{self, random_block_range, BlockRangeParams}; + use revm_database::BundleState; + use std::collections::HashMap; + + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let slot = U256::from_be_bytes(*STORAGE); + let hashed_storage = keccak256(STORAGE); + let account: revm_state::AccountInfo = + Account { nonce: 1, balance: U256::from(1000), bytecode_hash: None }.into(); + + let mut rng = generators::rng(); + let blocks = random_block_range( + &mut rng, + 0..=5, + BlockRangeParams { parent: Some(B256::ZERO), tx_count: 0..1, ..Default::default() }, + ); + + let mut addr_storage = HashMap::default(); + addr_storage.insert(slot, (U256::ZERO, U256::from(100))); + + type Revert = Vec<(Address, Option>, Vec<(U256, U256)>)>; + let mut reverts: Vec = vec![Vec::new(); 6]; + reverts[3] = vec![(ADDRESS, Some(Some(account.clone())), vec![(slot, U256::ZERO)])]; + reverts[5] = vec![(ADDRESS, Some(Some(account.clone())), vec![(slot, U256::from(50))])]; + + let bundle = BundleState::new([(ADDRESS, None, Some(account), addr_storage)], reverts, []); + + let provider_rw = factory.provider_rw().unwrap(); + provider_rw + .append_blocks_with_state( + blocks + .into_iter() + .map(|b| b.try_recover().expect("failed to seal block with senders")) + .collect(), + &ExecutionOutcome { bundle, first_block: 0, ..Default::default() }, + Default::default(), + ) + .unwrap(); + + let hashed_address = keccak256(ADDRESS); + + provider_rw + .tx_ref() + .put::( + hashed_address, + StorageEntry { key: hashed_storage, value: U256::from(100) }, + ) + .unwrap(); + provider_rw + .tx_ref() + .put::( + hashed_address, + Account { nonce: 1, balance: U256::from(1000), bytecode_hash: None }, + ) + .unwrap(); + provider_rw.commit().unwrap(); + + let db = factory.provider().unwrap(); + + assert!(matches!( + HistoricalStateProviderRef::new(&db, 0).storage_by_hashed_key(ADDRESS, hashed_storage), + Ok(None) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 3).storage_by_hashed_key(ADDRESS, hashed_storage), + Ok(Some(U256::ZERO)) + )); + assert!(matches!( + HistoricalStateProviderRef::new(&db, 4).storage_by_hashed_key(ADDRESS, hashed_storage), + Ok(Some(v)) if v == U256::from(50) + )); + + assert!(matches!( + HistoricalStateProviderRef::new(&db, 4).storage_by_hashed_key(ADDRESS, STORAGE), + Ok(None | Some(U256::ZERO)) + )); + } } diff --git a/crates/storage/provider/src/providers/state/latest.rs b/crates/storage/provider/src/providers/state/latest.rs index 7c1964051e..2550c3c79a 100644 --- a/crates/storage/provider/src/providers/state/latest.rs +++ b/crates/storage/provider/src/providers/state/latest.rs @@ -4,7 +4,9 @@ use crate::{ use alloy_primitives::{Address, BlockNumber, Bytes, StorageKey, StorageValue, B256}; use reth_db_api::{cursor::DbDupCursorRO, tables, transaction::DbTx}; use reth_primitives_traits::{Account, Bytecode}; -use reth_storage_api::{BytecodeReader, DBProvider, StateProofProvider, StorageRootProvider}; +use reth_storage_api::{ + BytecodeReader, DBProvider, StateProofProvider, StorageRootProvider, StorageSettingsCache, +}; use reth_storage_errors::provider::{ProviderError, ProviderResult}; use reth_trie::{ proof::{Proof, StorageProof}, @@ -33,12 +35,33 @@ impl<'b, Provider: DBProvider> LatestStateProviderRef<'b, Provider> { fn tx(&self) -> &Provider::Tx { self.0.tx_ref() } + + fn hashed_storage_lookup( + &self, + hashed_address: B256, + hashed_slot: StorageKey, + ) -> ProviderResult> { + let mut cursor = self.tx().cursor_dup_read::()?; + Ok(cursor + .seek_by_key_subkey(hashed_address, hashed_slot)? + .filter(|e| e.key == hashed_slot) + .map(|e| e.value)) + } } -impl AccountReader for LatestStateProviderRef<'_, Provider> { +impl AccountReader + for LatestStateProviderRef<'_, Provider> +{ /// Get basic account information. fn basic_account(&self, address: &Address) -> ProviderResult> { - self.tx().get_by_encoded_key::(address).map_err(Into::into) + if self.0.cached_storage_settings().use_hashed_state { + let hashed_address = alloy_primitives::keccak256(address); + self.tx() + .get_by_encoded_key::(&hashed_address) + .map_err(Into::into) + } else { + self.tx().get_by_encoded_key::(address).map_err(Into::into) + } } } @@ -148,22 +171,41 @@ impl HashedPostStateProvider for LatestStateProviderRef<'_ } } -impl StateProvider +impl StateProvider for LatestStateProviderRef<'_, Provider> { - /// Get storage. + /// Get storage by plain (unhashed) storage key slot. fn storage( &self, account: Address, storage_key: StorageKey, ) -> ProviderResult> { - let mut cursor = self.tx().cursor_dup_read::()?; - if let Some(entry) = cursor.seek_by_key_subkey(account, storage_key)? && - entry.key == storage_key - { - return Ok(Some(entry.value)) + if self.0.cached_storage_settings().use_hashed_state { + self.hashed_storage_lookup( + alloy_primitives::keccak256(account), + alloy_primitives::keccak256(storage_key), + ) + } else { + let mut cursor = self.tx().cursor_dup_read::()?; + if let Some(entry) = cursor.seek_by_key_subkey(account, storage_key)? && + entry.key == storage_key + { + return Ok(Some(entry.value)); + } + Ok(None) + } + } + + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: StorageKey, + ) -> ProviderResult> { + if self.0.cached_storage_settings().use_hashed_state { + self.hashed_storage_lookup(alloy_primitives::keccak256(address), hashed_storage_key) + } else { + Err(ProviderError::UnsupportedProvider) } - Ok(None) } } @@ -194,15 +236,181 @@ impl LatestStateProvider { } // Delegates all provider impls to [LatestStateProviderRef] -reth_storage_api::macros::delegate_provider_impls!(LatestStateProvider where [Provider: DBProvider + BlockHashReader ]); +reth_storage_api::macros::delegate_provider_impls!(LatestStateProvider where [Provider: DBProvider + BlockHashReader + StorageSettingsCache]); #[cfg(test)] mod tests { use super::*; + use crate::test_utils::create_test_provider_factory; + use alloy_primitives::{address, b256, keccak256, U256}; + use reth_db_api::{ + models::StorageSettings, + tables, + transaction::{DbTx, DbTxMut}, + }; + use reth_primitives_traits::StorageEntry; + use reth_storage_api::StorageSettingsCache; + use reth_storage_errors::provider::ProviderError; const fn assert_state_provider() {} #[expect(dead_code)] - const fn assert_latest_state_provider() { + const fn assert_latest_state_provider< + T: DBProvider + BlockHashReader + StorageSettingsCache, + >() { assert_state_provider::>(); } + + #[test] + fn test_latest_storage_hashed_state() { + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let address = address!("0x0000000000000000000000000000000000000001"); + let slot = b256!("0x0000000000000000000000000000000000000000000000000000000000000001"); + + let hashed_address = keccak256(address); + let hashed_slot = keccak256(slot); + + let tx = factory.provider_rw().unwrap().into_tx(); + tx.put::( + hashed_address, + StorageEntry { key: hashed_slot, value: U256::from(42) }, + ) + .unwrap(); + tx.commit().unwrap(); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + + assert_eq!(provider_ref.storage(address, slot).unwrap(), Some(U256::from(42))); + + let other_address = address!("0x0000000000000000000000000000000000000099"); + let other_slot = + b256!("0x0000000000000000000000000000000000000000000000000000000000000099"); + assert_eq!(provider_ref.storage(other_address, other_slot).unwrap(), None); + + let tx = factory.provider_rw().unwrap().into_tx(); + let plain_address = address!("0x0000000000000000000000000000000000000002"); + let plain_slot = + b256!("0x0000000000000000000000000000000000000000000000000000000000000002"); + tx.put::( + plain_address, + StorageEntry { key: plain_slot, value: U256::from(99) }, + ) + .unwrap(); + tx.commit().unwrap(); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + assert_eq!(provider_ref.storage(plain_address, plain_slot).unwrap(), None); + } + + #[test] + fn test_latest_storage_hashed_state_returns_none_for_missing() { + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let address = address!("0x0000000000000000000000000000000000000001"); + let slot = b256!("0x0000000000000000000000000000000000000000000000000000000000000001"); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + assert_eq!(provider_ref.storage(address, slot).unwrap(), None); + } + + #[test] + fn test_latest_storage_legacy() { + let factory = create_test_provider_factory(); + assert!(!factory.provider().unwrap().cached_storage_settings().use_hashed_state); + + let address = address!("0x0000000000000000000000000000000000000001"); + let slot = b256!("0x0000000000000000000000000000000000000000000000000000000000000005"); + + let tx = factory.provider_rw().unwrap().into_tx(); + tx.put::( + address, + StorageEntry { key: slot, value: U256::from(42) }, + ) + .unwrap(); + tx.commit().unwrap(); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + + assert_eq!(provider_ref.storage(address, slot).unwrap(), Some(U256::from(42))); + + let other_slot = + b256!("0x0000000000000000000000000000000000000000000000000000000000000099"); + assert_eq!(provider_ref.storage(address, other_slot).unwrap(), None); + } + + #[test] + fn test_latest_storage_legacy_does_not_read_hashed() { + let factory = create_test_provider_factory(); + assert!(!factory.provider().unwrap().cached_storage_settings().use_hashed_state); + + let address = address!("0x0000000000000000000000000000000000000001"); + let slot = b256!("0x0000000000000000000000000000000000000000000000000000000000000005"); + let hashed_address = keccak256(address); + let hashed_slot = keccak256(slot); + + let tx = factory.provider_rw().unwrap().into_tx(); + tx.put::( + hashed_address, + StorageEntry { key: hashed_slot, value: U256::from(42) }, + ) + .unwrap(); + tx.commit().unwrap(); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + assert_eq!(provider_ref.storage(address, slot).unwrap(), None); + } + + #[test] + fn test_latest_storage_by_hashed_key_v2() { + let factory = create_test_provider_factory(); + factory.set_storage_settings_cache(StorageSettings::v2()); + + let address = address!("0x0000000000000000000000000000000000000001"); + let slot = b256!("0x0000000000000000000000000000000000000000000000000000000000000001"); + + let hashed_address = keccak256(address); + let hashed_slot = keccak256(slot); + + let tx = factory.provider_rw().unwrap().into_tx(); + tx.put::( + hashed_address, + StorageEntry { key: hashed_slot, value: U256::from(42) }, + ) + .unwrap(); + tx.commit().unwrap(); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + + assert_eq!( + provider_ref.storage_by_hashed_key(address, hashed_slot).unwrap(), + Some(U256::from(42)) + ); + + assert_eq!(provider_ref.storage_by_hashed_key(address, slot).unwrap(), None); + } + + #[test] + fn test_latest_storage_by_hashed_key_unsupported_in_v1() { + let factory = create_test_provider_factory(); + assert!(!factory.provider().unwrap().cached_storage_settings().use_hashed_state); + + let address = address!("0x0000000000000000000000000000000000000001"); + let slot = b256!("0x0000000000000000000000000000000000000000000000000000000000000001"); + + let db = factory.provider().unwrap(); + let provider_ref = LatestStateProviderRef::new(&db); + + assert!(matches!( + provider_ref.storage_by_hashed_key(address, slot), + Err(ProviderError::UnsupportedProvider) + )); + } } diff --git a/crates/storage/provider/src/providers/state/overlay.rs b/crates/storage/provider/src/providers/state/overlay.rs index 9074a3da38..350eeaf388 100644 --- a/crates/storage/provider/src/providers/state/overlay.rs +++ b/crates/storage/provider/src/providers/state/overlay.rs @@ -10,17 +10,15 @@ use reth_stages_types::StageId; use reth_storage_api::{ BlockNumReader, ChangeSetReader, DBProvider, DatabaseProviderFactory, DatabaseProviderROFactory, PruneCheckpointReader, StageCheckpointReader, - StorageChangeSetReader, + StorageChangeSetReader, StorageSettingsCache, }; use reth_trie::{ hashed_cursor::{HashedCursorFactory, HashedPostStateCursorFactory}, trie_cursor::{InMemoryTrieCursorFactory, TrieCursorFactory}, updates::TrieUpdatesSorted, - HashedPostStateSorted, KeccakKeyHasher, -}; -use reth_trie_db::{ - ChangesetCache, DatabaseHashedCursorFactory, DatabaseHashedPostState, DatabaseTrieCursorFactory, + HashedPostStateSorted, }; +use reth_trie_db::{ChangesetCache, DatabaseHashedCursorFactory, DatabaseTrieCursorFactory}; use std::{ sync::Arc, time::{Duration, Instant}, @@ -198,7 +196,8 @@ where + ChangeSetReader + StorageChangeSetReader + DBProvider - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, { /// Resolves the effective overlay (trie updates, hashed state). /// @@ -336,10 +335,7 @@ where let _guard = debug_span!(target: "providers::state::overlay", "Retrieving hashed state reverts").entered(); let start = Instant::now(); - let res = HashedPostStateSorted::from_reverts::( - provider, - from_block + 1.., - )?; + let res = reth_trie_db::from_reverts_auto(provider, from_block + 1..)?; retrieve_hashed_state_reverts_duration = start.elapsed(); res }; @@ -450,7 +446,8 @@ where + PruneCheckpointReader + BlockNumReader + ChangeSetReader - + StorageChangeSetReader, + + StorageChangeSetReader + + StorageSettingsCache, { type Provider = OverlayStateProvider; diff --git a/crates/storage/provider/src/providers/static_file/manager.rs b/crates/storage/provider/src/providers/static_file/manager.rs index 8c94a2f6ce..e1cc9734a6 100644 --- a/crates/storage/provider/src/providers/static_file/manager.rs +++ b/crates/storage/provider/src/providers/static_file/manager.rs @@ -34,7 +34,7 @@ use reth_nippy_jar::{NippyJar, NippyJarChecker, CONFIG_FILE_EXTENSION}; use reth_node_types::NodePrimitives; use reth_primitives_traits::{ dashmap::DashMap, AlloyBlockHeader as _, BlockBody as _, RecoveredBlock, SealedHeader, - SignedTransaction, StorageEntry, + SignedTransaction, StorageSlotKey, }; use reth_prune_types::PruneSegment; use reth_stages_types::PipelineTarget; @@ -43,7 +43,7 @@ use reth_static_file_types::{ SegmentRangeInclusive, StaticFileMap, StaticFileSegment, DEFAULT_BLOCKS_PER_STATIC_FILE, }; use reth_storage_api::{ - BlockBodyIndicesProvider, ChangeSetReader, DBProvider, PruneCheckpointReader, + BlockBodyIndicesProvider, ChangeSetReader, ChangesetEntry, DBProvider, PruneCheckpointReader, StorageChangeSetReader, StorageSettingsCache, }; use reth_storage_errors::provider::{ProviderError, ProviderResult, StaticFileWriterError}; @@ -643,7 +643,7 @@ impl StaticFileProvider { revert.storage_revert.into_iter().map(move |(key, revert_to_slot)| { StorageBeforeTx { address: revert.address, - key: B256::new(key.to_be_bytes()), + key: StorageSlotKey::from_u256(key).to_hashed(), value: revert_to_slot.to_previous_value(), } }) @@ -2520,7 +2520,7 @@ impl StorageChangeSetReader for StaticFileProvider { fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { let provider = match self.get_segment_provider_for_block( StaticFileSegment::StorageChangeSets, block_number, @@ -2538,7 +2538,10 @@ impl StorageChangeSetReader for StaticFileProvider { for i in offset.changeset_range() { if let Some(change) = cursor.get_one::(i.into())? { let block_address = BlockNumberAddress((block_number, change.address)); - let entry = StorageEntry { key: change.key, value: change.value }; + let entry = ChangesetEntry { + key: StorageSlotKey::hashed(change.key), + value: change.value, + }; changeset.push((block_address, entry)); } } @@ -2553,7 +2556,7 @@ impl StorageChangeSetReader for StaticFileProvider { block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { let provider = match self.get_segment_provider_for_block( StaticFileSegment::StorageChangeSets, block_number, @@ -2602,7 +2605,10 @@ impl StorageChangeSetReader for StaticFileProvider { .get_one::(low.into())? .filter(|change| change.address == address && change.key == storage_key) { - return Ok(Some(StorageEntry { key: change.key, value: change.value })); + return Ok(Some(ChangesetEntry { + key: StorageSlotKey::hashed(change.key), + value: change.value, + })); } Ok(None) @@ -2611,7 +2617,7 @@ impl StorageChangeSetReader for StaticFileProvider { fn storage_changesets_range( &self, range: impl RangeBounds, - ) -> ProviderResult> { + ) -> ProviderResult> { let range = self.bound_range(range, StaticFileSegment::StorageChangeSets); self.walk_storage_changeset_range(range).collect() } diff --git a/crates/storage/provider/src/providers/static_file/mod.rs b/crates/storage/provider/src/providers/static_file/mod.rs index 50cd204df2..3a1673a4f7 100644 --- a/crates/storage/provider/src/providers/static_file/mod.rs +++ b/crates/storage/provider/src/providers/static_file/mod.rs @@ -1170,13 +1170,13 @@ mod tests { let result = sf_rw.get_storage_before_block(0, test_address, test_key).unwrap(); assert!(result.is_some()); let entry = result.unwrap(); - assert_eq!(entry.key, test_key); + assert_eq!(entry.key.as_b256(), test_key); assert_eq!(entry.value, U256::ZERO); let result = sf_rw.get_storage_before_block(2, test_address, test_key).unwrap(); assert!(result.is_some()); let entry = result.unwrap(); - assert_eq!(entry.key, test_key); + assert_eq!(entry.key.as_b256(), test_key); assert_eq!(entry.value, U256::from(9)); let result = sf_rw.get_storage_before_block(1, test_address, test_key).unwrap(); @@ -1188,7 +1188,7 @@ mod tests { let result = sf_rw.get_storage_before_block(1, other_address, other_key).unwrap(); assert!(result.is_some()); let entry = result.unwrap(); - assert_eq!(entry.key, other_key); + assert_eq!(entry.key.as_b256(), other_key); } } @@ -1334,20 +1334,20 @@ mod tests { let result = sf_rw.get_storage_before_block(block_num, address, keys[0]).unwrap(); assert!(result.is_some()); let entry = result.unwrap(); - assert_eq!(entry.key, keys[0]); + assert_eq!(entry.key.as_b256(), keys[0]); assert_eq!(entry.value, U256::from(0)); let result = sf_rw.get_storage_before_block(block_num, address, keys[num_slots - 1]).unwrap(); assert!(result.is_some()); let entry = result.unwrap(); - assert_eq!(entry.key, keys[num_slots - 1]); + assert_eq!(entry.key.as_b256(), keys[num_slots - 1]); let mid = num_slots / 2; let result = sf_rw.get_storage_before_block(block_num, address, keys[mid]).unwrap(); assert!(result.is_some()); let entry = result.unwrap(); - assert_eq!(entry.key, keys[mid]); + assert_eq!(entry.key.as_b256(), keys[mid]); let missing_key = B256::with_last_byte(255); let result = sf_rw.get_storage_before_block(block_num, address, missing_key).unwrap(); @@ -1356,7 +1356,7 @@ mod tests { for i in (0..num_slots).step_by(10) { let result = sf_rw.get_storage_before_block(block_num, address, keys[i]).unwrap(); assert!(result.is_some()); - assert_eq!(result.unwrap().key, keys[i]); + assert_eq!(result.unwrap().key.as_b256(), keys[i]); } } } diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index 16a89b83d2..a4ed876489 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -22,20 +22,20 @@ use reth_chainspec::{ChainInfo, EthChainSpec}; use reth_db::transaction::DbTx; use reth_db_api::{ mock::{DatabaseMock, TxMock}, - models::{AccountBeforeTx, StoredBlockBodyIndices}, + models::{AccountBeforeTx, StorageSettings, StoredBlockBodyIndices}, }; use reth_ethereum_primitives::EthPrimitives; use reth_execution_types::ExecutionOutcome; use reth_primitives_traits::{ Account, Block, BlockBody, Bytecode, GotExpected, NodePrimitives, RecoveredBlock, SealedHeader, - SignerRecoverable, StorageEntry, + SignerRecoverable, }; use reth_prune_types::{PruneCheckpoint, PruneModes, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_storage_api::{ - BlockBodyIndicesProvider, BytecodeReader, DBProvider, DatabaseProviderFactory, + BlockBodyIndicesProvider, BytecodeReader, ChangesetEntry, DBProvider, DatabaseProviderFactory, HashedPostStateProvider, NodePrimitivesProvider, StageCheckpointReader, StateProofProvider, - StorageChangeSetReader, StorageRootProvider, + StorageChangeSetReader, StorageRootProvider, StorageSettingsCache, }; use reth_storage_errors::provider::{ConsistentViewError, ProviderError, ProviderResult}; use reth_trie::{ @@ -883,6 +883,14 @@ where let lock = self.accounts.lock(); Ok(lock.get(&account).and_then(|account| account.storage.get(&storage_key)).copied()) } + + fn storage_by_hashed_key( + &self, + _address: Address, + _hashed_storage_key: StorageKey, + ) -> ProviderResult> { + Ok(None) + } } impl BytecodeReader for MockEthProvider @@ -903,6 +911,16 @@ where } } +impl StorageSettingsCache + for MockEthProvider +{ + fn cached_storage_settings(&self) -> StorageSettings { + StorageSettings::default() + } + + fn set_storage_settings_cache(&self, _settings: StorageSettings) {} +} + impl StateProviderFactory for MockEthProvider { @@ -1011,7 +1029,7 @@ impl StorageChangeSetReader fn storage_changeset( &self, _block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { Ok(Vec::default()) } @@ -1020,14 +1038,14 @@ impl StorageChangeSetReader _block_number: BlockNumber, _address: Address, _storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { Ok(None) } fn storage_changesets_range( &self, _range: impl RangeBounds, - ) -> ProviderResult> { + ) -> ProviderResult> { Ok(Vec::default()) } diff --git a/crates/storage/provider/src/traits/full.rs b/crates/storage/provider/src/traits/full.rs index 8fb9c38706..928ab55a56 100644 --- a/crates/storage/provider/src/traits/full.rs +++ b/crates/storage/provider/src/traits/full.rs @@ -10,7 +10,7 @@ use reth_chain_state::{ CanonStateSubscriptions, ForkChoiceSubscriptions, PersistedBlockSubscriptions, }; use reth_node_types::{BlockTy, HeaderTy, NodeTypesWithDB, ReceiptTy, TxTy}; -use reth_storage_api::{NodePrimitivesProvider, StorageChangeSetReader}; +use reth_storage_api::{NodePrimitivesProvider, StorageChangeSetReader, StorageSettingsCache}; use std::fmt::Debug; /// Helper trait to unify all provider traits for simplicity. @@ -21,7 +21,8 @@ pub trait FullProvider: + StageCheckpointReader + PruneCheckpointReader + ChangeSetReader - + StorageChangeSetReader, + + StorageChangeSetReader + + StorageSettingsCache, > + NodePrimitivesProvider + StaticFileProviderFactory + RocksDBProviderFactory @@ -55,7 +56,8 @@ impl FullProvider for T where + StageCheckpointReader + PruneCheckpointReader + ChangeSetReader - + StorageChangeSetReader, + + StorageChangeSetReader + + StorageSettingsCache, > + NodePrimitivesProvider + StaticFileProviderFactory + RocksDBProviderFactory diff --git a/crates/storage/rpc-provider/src/lib.rs b/crates/storage/rpc-provider/src/lib.rs index 1a50a106f1..8771e5b215 100644 --- a/crates/storage/rpc-provider/src/lib.rs +++ b/crates/storage/rpc-provider/src/lib.rs @@ -1081,6 +1081,14 @@ where }) } + fn storage_by_hashed_key( + &self, + _address: Address, + _hashed_storage_key: StorageKey, + ) -> Result, ProviderError> { + Err(ProviderError::UnsupportedProvider) + } + fn account_code(&self, addr: &Address) -> Result, ProviderError> { self.block_on_async(async { let code = self diff --git a/crates/storage/storage-api/src/hashing.rs b/crates/storage/storage-api/src/hashing.rs index c6a02e4c3c..c0a6235c22 100644 --- a/crates/storage/storage-api/src/hashing.rs +++ b/crates/storage/storage-api/src/hashing.rs @@ -1,3 +1,4 @@ +use crate::ChangesetEntry; use alloc::collections::{BTreeMap, BTreeSet}; use alloy_primitives::{map::B256Map, Address, BlockNumber, B256}; use auto_impl::auto_impl; @@ -47,7 +48,7 @@ pub trait HashingWriter: Send { /// Mapping of hashed keys of updated accounts to their respective updated hashed slots. fn unwind_storage_hashing( &self, - changesets: impl Iterator, + changesets: impl Iterator, ) -> ProviderResult>>; /// Unwind and clear storage hashing in a given block range. diff --git a/crates/storage/storage-api/src/history.rs b/crates/storage/storage-api/src/history.rs index a06816a170..7acbd1083f 100644 --- a/crates/storage/storage-api/src/history.rs +++ b/crates/storage/storage-api/src/history.rs @@ -1,9 +1,9 @@ +use crate::ChangesetEntry; use alloy_primitives::{Address, BlockNumber, B256}; use auto_impl::auto_impl; use core::ops::{RangeBounds, RangeInclusive}; use reth_db_api::models::BlockNumberAddress; use reth_db_models::AccountBeforeTx; -use reth_primitives_traits::StorageEntry; use reth_storage_errors::provider::ProviderResult; /// History Writer @@ -36,7 +36,7 @@ pub trait HistoryWriter: Send { /// Returns number of changesets walked. fn unwind_storage_history_indices( &self, - changesets: impl Iterator, + changesets: impl Iterator, ) -> ProviderResult; /// Unwind and clear storage history indices in a given block range. diff --git a/crates/storage/storage-api/src/macros.rs b/crates/storage/storage-api/src/macros.rs index a299c529b8..42d8fbfe5f 100644 --- a/crates/storage/storage-api/src/macros.rs +++ b/crates/storage/storage-api/src/macros.rs @@ -41,6 +41,7 @@ macro_rules! delegate_provider_impls { } StateProvider $(where [$($generics)*])? { fn storage(&self, account: alloy_primitives::Address, storage_key: alloy_primitives::StorageKey) -> reth_storage_api::errors::provider::ProviderResult>; + fn storage_by_hashed_key(&self, address: alloy_primitives::Address, hashed_storage_key: alloy_primitives::StorageKey) -> reth_storage_api::errors::provider::ProviderResult>; } BytecodeReader $(where [$($generics)*])? { fn bytecode_by_hash(&self, code_hash: &alloy_primitives::B256) -> reth_storage_api::errors::provider::ProviderResult>; diff --git a/crates/storage/storage-api/src/noop.rs b/crates/storage/storage-api/src/noop.rs index 42620d9f83..728c91db93 100644 --- a/crates/storage/storage-api/src/noop.rs +++ b/crates/storage/storage-api/src/noop.rs @@ -10,7 +10,7 @@ use crate::{ }; #[cfg(feature = "db-api")] -use crate::{DBProvider, DatabaseProviderFactory, StorageChangeSetReader}; +use crate::{DBProvider, DatabaseProviderFactory, StorageChangeSetReader, StorageSettingsCache}; use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec}; use alloy_consensus::transaction::TransactionMeta; use alloy_eips::{BlockHashOrNumber, BlockId, BlockNumberOrTag}; @@ -413,9 +413,7 @@ impl StorageChangeSetReader for NoopProvider< fn storage_changeset( &self, _block_number: BlockNumber, - ) -> ProviderResult< - Vec<(reth_db_api::models::BlockNumberAddress, reth_primitives_traits::StorageEntry)>, - > { + ) -> ProviderResult> { Ok(Vec::default()) } @@ -424,16 +422,14 @@ impl StorageChangeSetReader for NoopProvider< _block_number: BlockNumber, _address: Address, _storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { Ok(None) } fn storage_changesets_range( &self, _range: impl core::ops::RangeBounds, - ) -> ProviderResult< - Vec<(reth_db_api::models::BlockNumberAddress, reth_primitives_traits::StorageEntry)>, - > { + ) -> ProviderResult> { Ok(Vec::default()) } @@ -542,6 +538,14 @@ impl StateProvider for NoopProvider { ) -> ProviderResult> { Ok(None) } + + fn storage_by_hashed_key( + &self, + _account: Address, + _hashed_storage_key: StorageKey, + ) -> ProviderResult> { + Err(ProviderError::UnsupportedProvider) + } } impl BytecodeReader for NoopProvider { @@ -695,3 +699,12 @@ impl DatabaseProviderFactory Ok(self.clone()) } } + +#[cfg(feature = "db-api")] +impl StorageSettingsCache for NoopProvider { + fn cached_storage_settings(&self) -> reth_db_api::models::StorageSettings { + reth_db_api::models::StorageSettings::default() + } + + fn set_storage_settings_cache(&self, _settings: reth_db_api::models::StorageSettings) {} +} diff --git a/crates/storage/storage-api/src/state.rs b/crates/storage/storage-api/src/state.rs index 22331953ef..3f16f6793b 100644 --- a/crates/storage/storage-api/src/state.rs +++ b/crates/storage/storage-api/src/state.rs @@ -41,12 +41,27 @@ pub trait StateProvider: + HashedPostStateProvider { /// Get storage of given account. + /// + /// When `use_hashed_state` is enabled, the `account` and `storage_key` are hashed internally + /// before lookup. Callers must pass **unhashed** (plain) values. fn storage( &self, account: Address, storage_key: StorageKey, ) -> ProviderResult>; + /// Get storage using a pre-hashed storage key. + /// + /// Unlike [`Self::storage`], `hashed_storage_key` must already be keccak256-hashed. + /// The `address` remains unhashed (plain) since history indices are keyed by plain address. + /// This is used when changeset keys are pre-hashed (e.g., `use_hashed_state` mode) + /// to avoid double-hashing. + fn storage_by_hashed_key( + &self, + address: Address, + hashed_storage_key: StorageKey, + ) -> ProviderResult>; + /// Get account code by its address. /// /// Returns `None` if the account doesn't exist or account is not a contract diff --git a/crates/storage/storage-api/src/storage.rs b/crates/storage/storage-api/src/storage.rs index ab92744970..513e3940e9 100644 --- a/crates/storage/storage-api/src/storage.rs +++ b/crates/storage/storage-api/src/storage.rs @@ -2,11 +2,38 @@ use alloc::{ collections::{BTreeMap, BTreeSet}, vec::Vec, }; -use alloy_primitives::{Address, BlockNumber, B256}; +use alloy_primitives::{Address, BlockNumber, B256, U256}; use core::ops::{RangeBounds, RangeInclusive}; -use reth_primitives_traits::StorageEntry; +use reth_primitives_traits::{StorageEntry, StorageSlotKey}; use reth_storage_errors::provider::ProviderResult; +/// A storage changeset entry whose key is tagged as [`StorageSlotKey::Plain`] or +/// [`StorageSlotKey::Hashed`] by the reader that produced it. +/// +/// Unlike [`StorageEntry`] (the raw DB row type with an untagged `B256` key), +/// this type carries provenance so downstream code can call +/// [`StorageSlotKey::to_hashed`] without consulting `StorageSettings`. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ChangesetEntry { + /// Storage slot key, tagged with its hashing status. + pub key: StorageSlotKey, + /// Value at this storage slot before the change. + pub value: U256, +} + +impl ChangesetEntry { + /// Convert to a raw [`StorageEntry`] (drops the tag). + pub const fn into_storage_entry(self) -> StorageEntry { + StorageEntry { key: self.key.as_b256(), value: self.value } + } +} + +impl From for StorageEntry { + fn from(e: ChangesetEntry) -> Self { + e.into_storage_entry() + } +} + /// Storage reader #[auto_impl::auto_impl(&, Box)] pub trait StorageReader: Send { @@ -37,26 +64,35 @@ pub trait StorageReader: Send { #[auto_impl::auto_impl(&, Box)] pub trait StorageChangeSetReader: Send { /// Iterate over storage changesets and return the storage state from before this block. + /// + /// Returned entries have their keys tagged as [`StorageSlotKey::Plain`] or + /// [`StorageSlotKey::Hashed`] based on the current storage mode. fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult>; + ) -> ProviderResult>; /// Search the block's changesets for the given address and storage key, and return the result. /// + /// The `storage_key` must match the key format used by the storage mode + /// (plain in v1, keccak256-hashed in v2). + /// /// Returns `None` if the storage slot was not changed in this block. fn get_storage_before_block( &self, block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult>; + ) -> ProviderResult>; /// Get all storage changesets in a range of blocks. + /// + /// Returned entries have their keys tagged as [`StorageSlotKey::Plain`] or + /// [`StorageSlotKey::Hashed`] based on the current storage mode. fn storage_changesets_range( &self, range: impl RangeBounds, - ) -> ProviderResult>; + ) -> ProviderResult>; /// Get the total count of all storage changes. fn storage_changeset_count(&self) -> ProviderResult; @@ -73,7 +109,7 @@ pub trait StorageChangeSetReader: Send { .into_iter() .map(|(block_address, entry)| reth_db_models::StorageBeforeTx { address: block_address.address(), - key: entry.key, + key: entry.key.as_b256(), value: entry.value, }) .collect() diff --git a/crates/trie/common/src/key.rs b/crates/trie/common/src/key.rs index 5a42bcd1f6..9082fa9378 100644 --- a/crates/trie/common/src/key.rs +++ b/crates/trie/common/src/key.rs @@ -16,3 +16,20 @@ impl KeyHasher for KeccakKeyHasher { keccak256(bytes) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_keccak_key_hasher_always_hashes_regardless_of_length() { + use alloy_primitives::Address; + + let addr = Address::repeat_byte(0x42); + assert_eq!(KeccakKeyHasher::hash_key(addr), keccak256(addr)); + + let slot = B256::repeat_byte(0x42); + assert_eq!(KeccakKeyHasher::hash_key(slot), keccak256(slot)); + assert_ne!(KeccakKeyHasher::hash_key(slot), slot); + } +} diff --git a/crates/trie/db/src/changesets.rs b/crates/trie/db/src/changesets.rs index 473c93c0c8..15bdb58d27 100644 --- a/crates/trie/db/src/changesets.rs +++ b/crates/trie/db/src/changesets.rs @@ -7,17 +7,18 @@ //! - **Reorg support**: Quickly access changesets to revert blocks during chain reorganizations //! - **Memory efficiency**: Automatic eviction ensures bounded memory usage -use crate::{DatabaseHashedPostState, DatabaseStateRoot, DatabaseTrieCursorFactory}; +use crate::{DatabaseStateRoot, DatabaseTrieCursorFactory}; use alloy_primitives::{map::B256Map, BlockNumber, B256}; use parking_lot::RwLock; use reth_storage_api::{ BlockNumReader, ChangeSetReader, DBProvider, StageCheckpointReader, StorageChangeSetReader, + StorageSettingsCache, }; use reth_storage_errors::provider::{ProviderError, ProviderResult}; use reth_trie::{ changesets::compute_trie_changesets, trie_cursor::{InMemoryTrieCursorFactory, TrieCursor, TrieCursorFactory}, - HashedPostStateSorted, KeccakKeyHasher, StateRoot, TrieInputSorted, + StateRoot, TrieInputSorted, }; use reth_trie_common::updates::{StorageTrieUpdatesSorted, TrieUpdatesSorted}; use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc, time::Instant}; @@ -66,7 +67,8 @@ where + StageCheckpointReader + ChangeSetReader + StorageChangeSetReader - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, { debug!( target: "trie::changeset_cache", @@ -77,14 +79,11 @@ where // Step 1: Collect/calculate state reverts // This is just the changes from this specific block - let individual_state_revert = HashedPostStateSorted::from_reverts::( - provider, - block_number..=block_number, - )?; + let individual_state_revert = + crate::state::from_reverts_auto(provider, block_number..=block_number)?; // This reverts all changes from db tip back to just after block was processed - let cumulative_state_revert = - HashedPostStateSorted::from_reverts::(provider, (block_number + 1)..)?; + let cumulative_state_revert = crate::state::from_reverts_auto(provider, (block_number + 1)..)?; // This reverts all changes from db tip back to just after block-1 was processed let mut cumulative_state_revert_prev = cumulative_state_revert.clone(); @@ -180,7 +179,8 @@ where + StageCheckpointReader + ChangeSetReader + StorageChangeSetReader - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, { let tx = provider.tx_ref(); @@ -334,7 +334,8 @@ impl ChangesetCache { + StageCheckpointReader + ChangeSetReader + StorageChangeSetReader - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, { // Try cache first (with read lock) { @@ -423,7 +424,8 @@ impl ChangesetCache { + StageCheckpointReader + ChangeSetReader + StorageChangeSetReader - + BlockNumReader, + + BlockNumReader + + StorageSettingsCache, { // Get the database tip block number let db_tip_block = provider diff --git a/crates/trie/db/src/lib.rs b/crates/trie/db/src/lib.rs index 16efa9fdb9..d6f8a5ed73 100644 --- a/crates/trie/db/src/lib.rs +++ b/crates/trie/db/src/lib.rs @@ -17,7 +17,7 @@ pub use hashed_cursor::{ }; pub use prefix_set::load_prefix_sets_with_provider; pub use proof::{DatabaseProof, DatabaseStorageProof}; -pub use state::{DatabaseHashedPostState, DatabaseStateRoot}; +pub use state::{from_reverts_auto, DatabaseHashedPostState, DatabaseStateRoot}; pub use storage::{hashed_storage_from_reverts_with_provider, DatabaseStorageRoot}; pub use trie_cursor::{ DatabaseAccountTrieCursor, DatabaseStorageTrieCursor, DatabaseTrieCursorFactory, diff --git a/crates/trie/db/src/prefix_set.rs b/crates/trie/db/src/prefix_set.rs index b0368477f4..440e9027cb 100644 --- a/crates/trie/db/src/prefix_set.rs +++ b/crates/trie/db/src/prefix_set.rs @@ -1,4 +1,5 @@ use alloy_primitives::{ + keccak256, map::{HashMap, HashSet}, BlockNumber, B256, }; @@ -9,23 +10,26 @@ use reth_db_api::{ tables, transaction::DbTx, }; -use reth_primitives_traits::StorageEntry; use reth_storage_api::{ChangeSetReader, DBProvider, StorageChangeSetReader}; use reth_storage_errors::provider::ProviderError; use reth_trie::{ prefix_set::{PrefixSetMut, TriePrefixSets}, - KeyHasher, Nibbles, + Nibbles, }; /// Load prefix sets using a provider that implements [`ChangeSetReader`]. This function can read /// changesets from both static files and database. -pub fn load_prefix_sets_with_provider( +/// +/// Storage keys from changesets are tagged as +/// [`Plain`](reth_primitives_traits::StorageSlotKey::Plain) +/// or [`Hashed`](reth_primitives_traits::StorageSlotKey::Hashed) by the reader, so callers need +/// not pass a `use_hashed_state` flag. Addresses are always hashed. +pub fn load_prefix_sets_with_provider( provider: &Provider, range: RangeInclusive, ) -> Result where Provider: ChangeSetReader + StorageChangeSetReader + DBProvider, - KH: KeyHasher, { let tx = provider.tx_ref(); @@ -41,7 +45,7 @@ where let mut account_hashed_state_cursor = tx.cursor_read::()?; for (_, AccountBeforeTx { address, .. }) in account_changesets { - let hashed_address = KH::hash_key(address); + let hashed_address = keccak256(address); account_prefix_set.insert(Nibbles::unpack(hashed_address)); if account_hashed_state_cursor.seek_exact(hashed_address)?.is_none() { @@ -51,13 +55,13 @@ where // Walk storage changesets using the provider (handles static files + database) let storage_changesets = provider.storage_changesets_range(range)?; - for (BlockNumberAddress((_, address)), StorageEntry { key, .. }) in storage_changesets { - let hashed_address = KH::hash_key(address); + for (BlockNumberAddress((_, address)), storage_entry) in storage_changesets { + let hashed_address = keccak256(address); account_prefix_set.insert(Nibbles::unpack(hashed_address)); storage_prefix_sets .entry(hashed_address) .or_default() - .insert(Nibbles::unpack(KH::hash_key(key))); + .insert(Nibbles::unpack(storage_entry.key.to_hashed())); } Ok(TriePrefixSets { diff --git a/crates/trie/db/src/state.rs b/crates/trie/db/src/state.rs index 3be7464a92..7203abdb2b 100644 --- a/crates/trie/db/src/state.rs +++ b/crates/trie/db/src/state.rs @@ -1,18 +1,18 @@ -use crate::{ - load_prefix_sets_with_provider, DatabaseHashedCursorFactory, DatabaseTrieCursorFactory, -}; -use alloy_primitives::{map::B256Map, BlockNumber, B256}; +use crate::{DatabaseHashedCursorFactory, DatabaseTrieCursorFactory}; +use alloy_primitives::{keccak256, map::B256Map, BlockNumber, B256}; use reth_db_api::{ models::{AccountBeforeTx, BlockNumberAddress}, transaction::DbTx, }; use reth_execution_errors::StateRootError; -use reth_storage_api::{BlockNumReader, ChangeSetReader, DBProvider, StorageChangeSetReader}; +use reth_storage_api::{ + BlockNumReader, ChangeSetReader, DBProvider, StorageChangeSetReader, StorageSettingsCache, +}; use reth_storage_errors::provider::ProviderError; use reth_trie::{ hashed_cursor::HashedPostStateCursorFactory, trie_cursor::InMemoryTrieCursorFactory, - updates::TrieUpdates, HashedPostStateSorted, HashedStorageSorted, KeccakKeyHasher, KeyHasher, - StateRoot, StateRootProgress, TrieInputSorted, + updates::TrieUpdates, HashedPostStateSorted, HashedStorageSorted, StateRoot, StateRootProgress, + TrieInputSorted, }; use std::{ collections::HashSet, @@ -32,7 +32,10 @@ pub trait DatabaseStateRoot<'a, TX>: Sized { /// /// An instance of state root calculator with account and storage prefixes loaded. fn incremental_root_calculator( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result; @@ -43,7 +46,10 @@ pub trait DatabaseStateRoot<'a, TX>: Sized { /// /// The updated state root. fn incremental_root( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result; @@ -56,7 +62,10 @@ pub trait DatabaseStateRoot<'a, TX>: Sized { /// /// The updated state root and the trie updates. fn incremental_root_with_updates( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result<(B256, TrieUpdates), StateRootError>; @@ -67,7 +76,10 @@ pub trait DatabaseStateRoot<'a, TX>: Sized { /// /// The intermediate progress of state root computation. fn incremental_root_with_progress( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result; @@ -130,7 +142,12 @@ pub trait DatabaseStateRoot<'a, TX>: Sized { pub trait DatabaseHashedPostState: Sized { /// Initializes [`HashedPostStateSorted`] from reverts. Iterates over state reverts in the /// specified range and aggregates them into sorted hashed state. - fn from_reverts( + /// + /// Storage keys from changesets are tagged as + /// [`Plain`](reth_primitives_traits::StorageSlotKey::Plain) or + /// [`Hashed`](reth_primitives_traits::StorageSlotKey::Hashed) by the reader, so no + /// `use_hashed_state` flag is needed. Addresses are always hashed. + fn from_reverts( provider: &(impl ChangeSetReader + StorageChangeSetReader + BlockNumReader + DBProvider), range: impl RangeBounds, ) -> Result; @@ -144,16 +161,22 @@ impl<'a, TX: DbTx> DatabaseStateRoot<'a, TX> } fn incremental_root_calculator( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result { let loaded_prefix_sets = - load_prefix_sets_with_provider::<_, KeccakKeyHasher>(provider, range)?; + crate::prefix_set::load_prefix_sets_with_provider(provider, range)?; Ok(Self::from_tx(provider.tx_ref()).with_prefix_sets(loaded_prefix_sets)) } fn incremental_root( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result { debug!(target: "trie::loader", ?range, "incremental state root"); @@ -161,7 +184,10 @@ impl<'a, TX: DbTx> DatabaseStateRoot<'a, TX> } fn incremental_root_with_updates( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result<(B256, TrieUpdates), StateRootError> { debug!(target: "trie::loader", ?range, "incremental state root"); @@ -169,7 +195,10 @@ impl<'a, TX: DbTx> DatabaseStateRoot<'a, TX> } fn incremental_root_with_progress( - provider: &'a (impl ChangeSetReader + StorageChangeSetReader + DBProvider), + provider: &'a (impl ChangeSetReader + + StorageChangeSetReader + + StorageSettingsCache + + DBProvider), range: RangeInclusive, ) -> Result { debug!(target: "trie::loader", ?range, "incremental state root with progress"); @@ -236,6 +265,21 @@ impl<'a, TX: DbTx> DatabaseStateRoot<'a, TX> } } +/// Calls [`HashedPostStateSorted::from_reverts`]. +/// +/// This is a convenience wrapper kept for backward compatibility. The storage +/// key tagging is now handled internally by the changeset reader. +pub fn from_reverts_auto( + provider: &(impl ChangeSetReader + + StorageChangeSetReader + + BlockNumReader + + DBProvider + + StorageSettingsCache), + range: impl RangeBounds, +) -> Result { + HashedPostStateSorted::from_reverts(provider, range) +} + impl DatabaseHashedPostState for HashedPostStateSorted { /// Builds a sorted hashed post-state from reverts. /// @@ -243,9 +287,12 @@ impl DatabaseHashedPostState for HashedPostStateSorted { /// This avoids intermediate `HashMap` allocations since MDBX data is already sorted. /// /// - Reads the first occurrence of each changed account/storage slot in the range. - /// - Hashes keys and returns them already ordered for trie iteration. + /// - Addresses are always keccak256-hashed. + /// - Storage keys are tagged by the changeset reader and hashed via + /// [`StorageSlotKey::to_hashed`](reth_primitives_traits::StorageSlotKey::to_hashed). + /// - Returns keys already ordered for trie iteration. #[instrument(target = "trie::db", skip(provider), fields(range))] - fn from_reverts( + fn from_reverts( provider: &(impl ChangeSetReader + StorageChangeSetReader + BlockNumReader + DBProvider), range: impl RangeBounds, ) -> Result { @@ -268,7 +315,7 @@ impl DatabaseHashedPostState for HashedPostStateSorted { for entry in provider.account_changesets_range(start..end)? { let (_, AccountBeforeTx { address, info }) = entry; if seen_accounts.insert(address) { - accounts.push((KH::hash_key(address), info)); + accounts.push((keccak256(address), info)); } } accounts.sort_unstable_by_key(|(hash, _)| *hash); @@ -283,12 +330,12 @@ impl DatabaseHashedPostState for HashedPostStateSorted { for (BlockNumberAddress((_, address)), storage) in provider.storage_changesets_range(start..=end_inclusive)? { - if seen_storage_keys.insert((address, storage.key)) { - let hashed_address = KH::hash_key(address); + if seen_storage_keys.insert((address, storage.key.as_b256())) { + let hashed_address = keccak256(address); storages .entry(hashed_address) .or_default() - .push((KH::hash_key(storage.key), storage.value)); + .push((storage.key.to_hashed(), storage.value)); } } } @@ -309,7 +356,7 @@ impl DatabaseHashedPostState for HashedPostStateSorted { #[cfg(test)] mod tests { use super::*; - use alloy_primitives::{hex, map::HashMap, Address, B256, U256}; + use alloy_primitives::{hex, keccak256, map::HashMap, Address, B256, U256}; use reth_db::test_utils::create_test_rw_db; use reth_db_api::{ database::Database, @@ -438,12 +485,11 @@ mod tests { ) .unwrap(); - let sorted = - HashedPostStateSorted::from_reverts::(&*provider, 1..=3).unwrap(); + let sorted = HashedPostStateSorted::from_reverts(&*provider, 1..=3).unwrap(); // Verify first occurrences were kept (nonce 1, not 2) assert_eq!(sorted.accounts.len(), 2); - let hashed_addr1 = KeccakKeyHasher::hash_key(address1); + let hashed_addr1 = keccak256(address1); let account1 = sorted.accounts.iter().find(|(addr, _)| *addr == hashed_addr1).unwrap(); assert_eq!(account1.1.unwrap().nonce, 1); @@ -475,9 +521,225 @@ mod tests { .unwrap(); // Query a range with no data - let sorted = - HashedPostStateSorted::from_reverts::(&*provider, 1..=10).unwrap(); + let sorted = HashedPostStateSorted::from_reverts(&*provider, 1..=10).unwrap(); assert!(sorted.accounts.is_empty()); assert!(sorted.storages.is_empty()); } + + #[test] + fn from_reverts_with_hashed_state() { + use reth_db_api::models::StorageBeforeTx; + use reth_provider::{StaticFileProviderFactory, StaticFileSegment, StaticFileWriter}; + + let factory = create_test_provider_factory(); + + let mut settings = factory.cached_storage_settings(); + settings.use_hashed_state = true; + settings.storage_changesets_in_static_files = true; + factory.set_storage_settings_cache(settings); + + let provider = factory.provider_rw().unwrap(); + + let address1 = Address::with_last_byte(1); + let address2 = Address::with_last_byte(2); + + let plain_slot1 = B256::from(U256::from(11)); + let plain_slot2 = B256::from(U256::from(22)); + let hashed_slot1 = keccak256(plain_slot1); + let hashed_slot2 = keccak256(plain_slot2); + + provider + .tx_ref() + .put::( + 1, + AccountBeforeTx { + address: address1, + info: Some(Account { nonce: 1, ..Default::default() }), + }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + 2, + AccountBeforeTx { + address: address1, + info: Some(Account { nonce: 2, ..Default::default() }), + }, + ) + .unwrap(); + provider + .tx_ref() + .put::(3, AccountBeforeTx { address: address2, info: None }) + .unwrap(); + + { + let sf = factory.static_file_provider(); + let mut writer = sf.latest_writer(StaticFileSegment::StorageChangeSets).unwrap(); + writer.append_storage_changeset(vec![], 0).unwrap(); + writer + .append_storage_changeset( + vec![StorageBeforeTx { + address: address1, + key: hashed_slot2, + value: U256::from(200), + }], + 1, + ) + .unwrap(); + writer + .append_storage_changeset( + vec![StorageBeforeTx { + address: address1, + key: hashed_slot1, + value: U256::from(100), + }], + 2, + ) + .unwrap(); + writer + .append_storage_changeset( + vec![StorageBeforeTx { + address: address1, + key: hashed_slot1, + value: U256::from(999), + }], + 3, + ) + .unwrap(); + writer.commit().unwrap(); + } + + let sorted = HashedPostStateSorted::from_reverts(&*provider, 1..=3).unwrap(); + + assert_eq!(sorted.accounts.len(), 2); + + let hashed_addr1 = keccak256(address1); + let hashed_addr2 = keccak256(address2); + + let account1 = sorted.accounts.iter().find(|(addr, _)| *addr == hashed_addr1).unwrap(); + assert_eq!(account1.1.unwrap().nonce, 1); + + let account2 = sorted.accounts.iter().find(|(addr, _)| *addr == hashed_addr2).unwrap(); + assert!(account2.1.is_none()); + + assert!(sorted.accounts.windows(2).all(|w| w[0].0 <= w[1].0)); + + let storage = sorted.storages.get(&hashed_addr1).expect("storage for address1"); + assert_eq!(storage.storage_slots.len(), 2); + + let found_slot1 = storage.storage_slots.iter().find(|(k, _)| *k == hashed_slot1).unwrap(); + assert_eq!(found_slot1.1, U256::from(100)); + + let found_slot2 = storage.storage_slots.iter().find(|(k, _)| *k == hashed_slot2).unwrap(); + assert_eq!(found_slot2.1, U256::from(200)); + + assert_ne!(hashed_slot1, plain_slot1); + assert_ne!(hashed_slot2, plain_slot2); + + assert!(storage.storage_slots.windows(2).all(|w| w[0].0 <= w[1].0)); + } + + #[test] + fn from_reverts_legacy_keccak_hashes_all_keys() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + + let address1 = Address::with_last_byte(1); + let address2 = Address::with_last_byte(2); + let plain_slot1 = B256::from(U256::from(11)); + let plain_slot2 = B256::from(U256::from(22)); + + provider + .tx_ref() + .put::( + 1, + AccountBeforeTx { + address: address1, + info: Some(Account { nonce: 10, ..Default::default() }), + }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + 2, + AccountBeforeTx { + address: address2, + info: Some(Account { nonce: 20, ..Default::default() }), + }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + 3, + AccountBeforeTx { + address: address1, + info: Some(Account { nonce: 99, ..Default::default() }), + }, + ) + .unwrap(); + + provider + .tx_ref() + .put::( + BlockNumberAddress((1, address1)), + StorageEntry { key: plain_slot1, value: U256::from(100) }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + BlockNumberAddress((2, address1)), + StorageEntry { key: plain_slot2, value: U256::from(200) }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + BlockNumberAddress((3, address2)), + StorageEntry { key: plain_slot1, value: U256::from(300) }, + ) + .unwrap(); + + let sorted = HashedPostStateSorted::from_reverts(&*provider, 1..=3).unwrap(); + + let expected_hashed_addr1 = keccak256(address1); + let expected_hashed_addr2 = keccak256(address2); + assert_eq!(sorted.accounts.len(), 2); + + let account1 = + sorted.accounts.iter().find(|(addr, _)| *addr == expected_hashed_addr1).unwrap(); + assert_eq!(account1.1.unwrap().nonce, 10); + + let account2 = + sorted.accounts.iter().find(|(addr, _)| *addr == expected_hashed_addr2).unwrap(); + assert_eq!(account2.1.unwrap().nonce, 20); + + assert!(sorted.accounts.windows(2).all(|w| w[0].0 <= w[1].0)); + + let expected_hashed_slot1 = keccak256(plain_slot1); + let expected_hashed_slot2 = keccak256(plain_slot2); + + assert_ne!(expected_hashed_slot1, plain_slot1); + assert_ne!(expected_hashed_slot2, plain_slot2); + + let storage1 = sorted.storages.get(&expected_hashed_addr1).expect("storage for address1"); + assert_eq!(storage1.storage_slots.len(), 2); + assert!(storage1 + .storage_slots + .iter() + .any(|(k, v)| *k == expected_hashed_slot1 && *v == U256::from(100))); + assert!(storage1 + .storage_slots + .iter() + .any(|(k, v)| *k == expected_hashed_slot2 && *v == U256::from(200))); + assert!(storage1.storage_slots.windows(2).all(|w| w[0].0 <= w[1].0)); + + let storage2 = sorted.storages.get(&expected_hashed_addr2).expect("storage for address2"); + assert_eq!(storage2.storage_slots.len(), 1); + assert_eq!(storage2.storage_slots[0].0, expected_hashed_slot1); + assert_eq!(storage2.storage_slots[0].1, U256::from(300)); + } } diff --git a/crates/trie/db/src/storage.rs b/crates/trie/db/src/storage.rs index cbaad8d031..748abe2b77 100644 --- a/crates/trie/db/src/storage.rs +++ b/crates/trie/db/src/storage.rs @@ -47,7 +47,7 @@ where provider.storage_changesets_range(from..=tip)? { if storage_address == address { - let hashed_slot = keccak256(storage_change.key); + let hashed_slot = storage_change.key.to_hashed(); if let hash_map::Entry::Vacant(entry) = storage.storage.entry(hashed_slot) { entry.insert(storage_change.value); } @@ -101,3 +101,131 @@ impl<'a, TX: DbTx> DatabaseStorageRoot<'a, TX> .root() } } + +#[cfg(test)] +mod tests { + use super::*; + use alloy_consensus::Header; + use alloy_primitives::U256; + use reth_db_api::{models::BlockNumberAddress, tables, transaction::DbTxMut}; + use reth_primitives_traits::StorageEntry; + use reth_provider::{ + test_utils::create_test_provider_factory, StaticFileProviderFactory, StaticFileSegment, + StaticFileWriter, StorageSettingsCache, + }; + + fn append_storage_changesets_to_static_files( + factory: &impl StaticFileProviderFactory< + Primitives: reth_primitives_traits::NodePrimitives, + >, + changesets: Vec<(u64, Vec)>, + ) { + let sf = factory.static_file_provider(); + let mut writer = sf.latest_writer(StaticFileSegment::StorageChangeSets).unwrap(); + for (block_number, changeset) in changesets { + writer.append_storage_changeset(changeset, block_number).unwrap(); + } + writer.commit().unwrap(); + } + + fn append_headers_to_static_files( + factory: &impl StaticFileProviderFactory< + Primitives: reth_primitives_traits::NodePrimitives, + >, + up_to_block: u64, + ) { + let sf = factory.static_file_provider(); + let mut writer = sf.latest_writer(StaticFileSegment::Headers).unwrap(); + let mut header = Header::default(); + for num in 0..=up_to_block { + header.number = num; + writer.append_header(&header, &B256::ZERO).unwrap(); + } + writer.commit().unwrap(); + } + + #[test] + fn test_hashed_storage_from_reverts_legacy() { + let factory = create_test_provider_factory(); + let provider = factory.provider_rw().unwrap(); + + assert!(!provider.cached_storage_settings().use_hashed_state); + + let address = Address::with_last_byte(42); + let slot1 = B256::from(U256::from(100)); + let slot2 = B256::from(U256::from(200)); + + append_headers_to_static_files(&factory, 5); + + provider + .tx_ref() + .put::( + BlockNumberAddress((1, address)), + StorageEntry { key: slot1, value: U256::from(10) }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + BlockNumberAddress((2, address)), + StorageEntry { key: slot2, value: U256::from(20) }, + ) + .unwrap(); + provider + .tx_ref() + .put::( + BlockNumberAddress((3, address)), + StorageEntry { key: slot1, value: U256::from(999) }, + ) + .unwrap(); + + let result = hashed_storage_from_reverts_with_provider(&*provider, address, 1).unwrap(); + + let hashed_slot1 = keccak256(slot1); + let hashed_slot2 = keccak256(slot2); + + assert_eq!(result.storage.len(), 2); + assert_eq!(result.storage.get(&hashed_slot1), Some(&U256::from(10))); + assert_eq!(result.storage.get(&hashed_slot2), Some(&U256::from(20))); + } + + #[test] + fn test_hashed_storage_from_reverts_hashed_state() { + use reth_db_api::models::StorageBeforeTx; + + let factory = create_test_provider_factory(); + + let mut settings = factory.cached_storage_settings(); + settings.use_hashed_state = true; + settings.storage_changesets_in_static_files = true; + factory.set_storage_settings_cache(settings); + + let provider = factory.provider_rw().unwrap(); + assert!(provider.cached_storage_settings().use_hashed_state); + assert!(provider.cached_storage_settings().storage_changesets_in_static_files); + + let address = Address::with_last_byte(42); + let plain_slot1 = B256::from(U256::from(100)); + let plain_slot2 = B256::from(U256::from(200)); + let hashed_slot1 = keccak256(plain_slot1); + let hashed_slot2 = keccak256(plain_slot2); + + append_headers_to_static_files(&factory, 5); + + append_storage_changesets_to_static_files( + &factory, + vec![ + (0, vec![]), + (1, vec![StorageBeforeTx { address, key: hashed_slot1, value: U256::from(10) }]), + (2, vec![StorageBeforeTx { address, key: hashed_slot2, value: U256::from(20) }]), + (3, vec![StorageBeforeTx { address, key: hashed_slot1, value: U256::from(999) }]), + ], + ); + + let result = hashed_storage_from_reverts_with_provider(&*provider, address, 1).unwrap(); + + assert_eq!(result.storage.len(), 2); + assert_eq!(result.storage.get(&hashed_slot1), Some(&U256::from(10))); + assert_eq!(result.storage.get(&hashed_slot2), Some(&U256::from(20))); + } +} diff --git a/docs/vocs/docs/pages/cli/reth/db/checksum/use_hashed_state.mdx b/docs/vocs/docs/pages/cli/reth/db/checksum/use_hashed_state.mdx new file mode 100644 index 0000000000..02605f5cbe --- /dev/null +++ b/docs/vocs/docs/pages/cli/reth/db/checksum/use_hashed_state.mdx @@ -0,0 +1,170 @@ +# op-reth db settings set use_hashed_state + +Use hashed state tables (HashedAccounts/HashedStorages) as canonical state + +```bash +$ op-reth db settings set use_hashed_state --help +``` +```txt +Usage: op-reth db settings set use_hashed_state [OPTIONS] + +Arguments: + + [possible values: true, false] + +Options: + -h, --help + Print help (see a summary with '-h') + +Datadir: + --chain + The chain this node is running. + Possible values are either a built-in chain or the path to a chain specification file. + + Built-in chains: + optimism, optimism_sepolia, optimism-sepolia, base, base_sepolia, base-sepolia, arena-z, arena-z-sepolia, automata, base-devnet-0-sepolia-dev-0, bob, boba-sepolia, boba, camp-sepolia, celo, creator-chain-testnet-sepolia, cyber, cyber-sepolia, ethernity, ethernity-sepolia, fraxtal, funki, funki-sepolia, hashkeychain, ink, ink-sepolia, lisk, lisk-sepolia, lyra, metal, metal-sepolia, mint, mode, mode-sepolia, oplabs-devnet-0-sepolia-dev-0, orderly, ozean-sepolia, pivotal-sepolia, polynomial, race, race-sepolia, radius_testnet-sepolia, redstone, rehearsal-0-bn-0-rehearsal-0-bn, rehearsal-0-bn-1-rehearsal-0-bn, settlus-mainnet, settlus-sepolia-sepolia, shape, shape-sepolia, silent-data-mainnet, snax, soneium, soneium-minato-sepolia, sseed, swan, swell, tbn, tbn-sepolia, unichain, unichain-sepolia, worldchain, worldchain-sepolia, xterio-eth, zora, zora-sepolia, dev + + [default: optimism] + +Logging: + --log.stdout.format + The format to use for logs written to stdout + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.stdout.filter + The filter to use for logs written to stdout + + [default: ] + + --log.file.format + The format to use for logs written to the log file + + Possible values: + - json: Represents JSON formatting for logs. This format outputs log records as JSON objects, making it suitable for structured logging + - log-fmt: Represents logfmt (key=value) formatting for logs. This format is concise and human-readable, typically used in command-line applications + - terminal: Represents terminal-friendly formatting for logs + + [default: terminal] + + --log.file.filter + The filter to use for logs written to the log file + + [default: debug] + + --log.file.directory + The path to put log files in + + [default: /logs] + + --log.file.name + The prefix name of the log files + + [default: reth.log] + + --log.file.max-size + The maximum size (in MB) of one log file + + [default: 200] + + --log.file.max-files + The maximum amount of log files that will be stored. If set to 0, background file logging is disabled + + [default: 5] + + --log.journald + Write logs to journald + + --log.journald.filter + The filter to use for logs written to journald + + [default: error] + + --color + Sets whether or not the formatter emits ANSI terminal escape codes for colors and other text formatting + + Possible values: + - always: Colors on + - auto: Auto-detect + - never: Colors off + + [default: always] + + --logs-otlp[=] + Enable `Opentelemetry` logs export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/logs` - gRPC: `http://localhost:4317` + + Example: --logs-otlp=http://collector:4318/v1/logs + + [env: OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=] + + --logs-otlp.filter + Set a filter directive for the OTLP logs exporter. This controls the verbosity of logs sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --logs-otlp.filter=info,reth=debug + + Defaults to INFO if not specified. + + [default: info] + +Display: + -v, --verbosity... + Set the minimum log level. + + -v Errors + -vv Warnings + -vvv Info + -vvvv Debug + -vvvvv Traces (warning: very verbose!) + + -q, --quiet + Silence all log output + +Tracing: + --tracing-otlp[=] + Enable `Opentelemetry` tracing export to an OTLP endpoint. + + If no value provided, defaults based on protocol: - HTTP: `http://localhost:4318/v1/traces` - gRPC: `http://localhost:4317` + + Example: --tracing-otlp=http://collector:4318/v1/traces + + [env: OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=] + + --tracing-otlp-protocol + OTLP transport protocol to use for exporting traces and logs. + + - `http`: expects endpoint path to end with `/v1/traces` or `/v1/logs` - `grpc`: expects endpoint without a path + + Defaults to HTTP if not specified. + + Possible values: + - http: HTTP/Protobuf transport, port 4318, requires `/v1/traces` path + - grpc: gRPC transport, port 4317 + + [env: OTEL_EXPORTER_OTLP_PROTOCOL=] + [default: http] + + --tracing-otlp.filter + Set a filter directive for the OTLP tracer. This controls the verbosity of spans and events sent to the OTLP endpoint. It follows the same syntax as the `RUST_LOG` environment variable. + + Example: --tracing-otlp.filter=info,reth=debug,hyper_util=off + + Defaults to TRACE if not specified. + + [default: debug] + + --tracing-otlp.sample-ratio + Trace sampling ratio to control the percentage of traces to export. + + Valid range: 0.0 to 1.0 - 1.0, default: Sample all traces - 0.01: Sample 1% of traces - 0.0: Disable sampling + + Example: --tracing-otlp.sample-ratio=0.0. + + [env: OTEL_TRACES_SAMPLER_ARG=] +``` \ No newline at end of file