diff --git a/Cargo.lock b/Cargo.lock index 7b5e0e2984..e159854891 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10131,6 +10131,7 @@ dependencies = [ "futures-util", "itertools 0.14.0", "num-traits", + "page_size", "paste", "rand 0.9.2", "rayon", @@ -10154,6 +10155,7 @@ dependencies = [ "reth-execution-types", "reth-exex", "reth-fs-util", + "reth-libmdbx", "reth-network-p2p", "reth-network-peers", "reth-primitives-traits", diff --git a/crates/chain-state/src/in_memory.rs b/crates/chain-state/src/in_memory.rs index 4eac2cb580..00598e413b 100644 --- a/crates/chain-state/src/in_memory.rs +++ b/crates/chain-state/src/in_memory.rs @@ -1061,14 +1061,6 @@ 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 f70fcbb89b..a4269886da 100644 --- a/crates/chain-state/src/memory_overlay.rs +++ b/crates/chain-state/src/memory_overlay.rs @@ -223,26 +223,6 @@ 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/get.rs b/crates/cli/commands/src/db/get.rs index 05f424c2e4..f0dfd5db79 100644 --- a/crates/cli/commands/src/db/get.rs +++ b/crates/cli/commands/src/db/get.rs @@ -98,7 +98,7 @@ impl Command { )?; if let Some(entry) = entry { - let se: reth_primitives_traits::StorageEntry = entry.into(); + let se: reth_primitives_traits::StorageEntry = entry; println!("{}", serde_json::to_string_pretty(&se)?); } else { error!(target: "reth::cli", "No content for the given table key."); @@ -110,7 +110,7 @@ impl Command { let serializable: Vec<_> = changesets .into_iter() .map(|(addr, entry)| { - let se: reth_primitives_traits::StorageEntry = entry.into(); + let se: reth_primitives_traits::StorageEntry = entry; (addr, se) }) .collect(); diff --git a/crates/cli/commands/src/stage/unwind.rs b/crates/cli/commands/src/stage/unwind.rs index 2e4e6a654b..2fdda790a0 100644 --- a/crates/cli/commands/src/stage/unwind.rs +++ b/crates/cli/commands/src/stage/unwind.rs @@ -52,7 +52,7 @@ impl> Command Comp: CliNodeComponents, F: FnOnce(Arc) -> Comp, { - let Environment { provider_factory, config, .. } = + let Environment { provider_factory, config, data_dir: _ } = self.env.init::(AccessRights::RW, runtime)?; let target = self.command.unwind_target(provider_factory.clone())?; diff --git a/crates/engine/tree/src/tree/cached_state.rs b/crates/engine/tree/src/tree/cached_state.rs index d61ce02152..2b85d70551 100644 --- a/crates/engine/tree/src/tree/cached_state.rs +++ b/crates/engine/tree/src/tree/cached_state.rs @@ -351,14 +351,6 @@ 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 09f15f5627..73c3791695 100644 --- a/crates/engine/tree/src/tree/instrumented_state.rs +++ b/crates/engine/tree/src/tree/instrumented_state.rs @@ -199,17 +199,6 @@ 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/primitives-traits/src/lib.rs b/crates/primitives-traits/src/lib.rs index 67a0e913e9..6925fb5ed1 100644 --- a/crates/primitives-traits/src/lib.rs +++ b/crates/primitives-traits/src/lib.rs @@ -168,7 +168,7 @@ pub use alloy_primitives::{logs_bloom, Log, LogData}; pub mod proofs; mod storage; -pub use storage::{StorageEntry, StorageSlotKey, ValueWithSubKey}; +pub use storage::{StorageEntry, ValueWithSubKey}; pub mod sync; diff --git a/crates/primitives-traits/src/storage.rs b/crates/primitives-traits/src/storage.rs index 91d69815a3..4383f03cf9 100644 --- a/crates/primitives-traits/src/storage.rs +++ b/crates/primitives-traits/src/storage.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{keccak256, B256, U256}; +use alloy_primitives::{B256, U256}; /// Trait for `DupSort` table values that contain a subkey. /// @@ -12,117 +12,6 @@ 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. @@ -142,14 +31,6 @@ 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 71dcf1e0b0..fee49ff91e 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.as_b256()), block_number); + highest_deleted_storages.insert((address, entry.key), 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.as_b256()), block_number); + highest_deleted_storages.insert((address, entry.key), 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 409101fa6b..b268808461 100644 --- a/crates/revm/src/test_utils.rs +++ b/crates/revm/src/test_utils.rs @@ -160,14 +160,6 @@ 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 8bfc091ce5..09e1b3db3c 100644 --- a/crates/rpc/rpc-eth-types/src/cache/db.rs +++ b/crates/rpc/rpc-eth-types/src/cache/db.rs @@ -154,14 +154,6 @@ 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/Cargo.toml b/crates/stages/stages/Cargo.toml index b6d80c9527..f696d7185e 100644 --- a/crates/stages/stages/Cargo.toml +++ b/crates/stages/stages/Cargo.toml @@ -13,13 +13,14 @@ workspace = true [dependencies] # reth -reth-chainspec = { workspace = true, optional = true } +reth-chainspec.workspace = true reth-codecs.workspace = true reth-config.workspace = true reth-consensus.workspace = true reth-db.workspace = true reth-db-api.workspace = true reth-etl.workspace = true +reth-libmdbx.workspace = true reth-evm = { workspace = true, features = ["metrics"] } reth-era-downloader.workspace = true reth-era-utils.workspace = true @@ -59,6 +60,7 @@ tracing.workspace = true thiserror.workspace = true itertools.workspace = true rayon.workspace = true +page_size.workspace = true num-traits.workspace = true tempfile = { workspace = true, optional = true } bincode.workspace = true @@ -100,14 +102,13 @@ criterion = { workspace = true, features = ["async_tokio"] } [features] test-utils = [ - "dep:reth-chainspec", "reth-network-p2p/test-utils", "reth-db/test-utils", "reth-provider/test-utils", "reth-stages-api/test-utils", "dep:reth-testing-utils", "dep:tempfile", - "reth-chainspec?/test-utils", + "reth-chainspec/test-utils", "reth-consensus/test-utils", "reth-evm/test-utils", "reth-downloaders/test-utils", diff --git a/crates/stages/stages/src/stages/execution.rs b/crates/stages/stages/src/stages/execution/mod.rs similarity index 92% rename from crates/stages/stages/src/stages/execution.rs rename to crates/stages/stages/src/stages/execution/mod.rs index cec8384dfe..2a05915391 100644 --- a/crates/stages/stages/src/stages/execution.rs +++ b/crates/stages/stages/src/stages/execution/mod.rs @@ -2,6 +2,7 @@ use crate::stages::MERKLE_STAGE_DEFAULT_INCREMENTAL_THRESHOLD; use alloy_consensus::BlockHeader; use alloy_primitives::BlockNumber; use num_traits::Zero; +use reth_chainspec::{ChainSpecProvider, EthereumHardforks}; use reth_config::config::ExecutionConfig; use reth_consensus::FullConsensus; use reth_db::{static_file::HeaderMask, tables}; @@ -13,7 +14,7 @@ use reth_provider::{ providers::{StaticFileProvider, StaticFileWriter}, BlockHashReader, BlockReader, DBProvider, EitherWriter, ExecutionOutcome, HeaderProvider, LatestStateProviderRef, OriginalValuesKnown, ProviderError, StateWriteConfig, StateWriter, - StaticFileProviderFactory, StatsReader, StorageSettingsCache, TransactionVariant, + StaticFileProviderFactory, StatsReader, StoragePath, StorageSettingsCache, TransactionVariant, }; use reth_revm::database::StateProviderDatabase; use reth_stages_api::{ @@ -35,6 +36,8 @@ use tracing::*; use super::missing_static_data_error; +mod slot_preimages; + /// The execution stage executes all transactions and /// update history indexes. /// @@ -268,7 +271,9 @@ where > + StatsReader + BlockHashReader + StateWriter::Receipt> - + StorageSettingsCache, + + StorageSettingsCache + + StoragePath + + ChainSpecProvider, { /// Return the id of the stage fn id(&self) -> StageId { @@ -462,6 +467,26 @@ where } } + // When using hashed state (storage.v2), inject plain storage-slot keys into wipe + // reverts for self-destructed accounts. Without this, the changeset writer would only + // see hashed slot keys (from `HashedStorages`) which pollutes the entire codebase. + // + // SELFDESTRUCT no longer destroys storage post-Cancun, so this is only needed for + // pre-Cancun blocks. Post-Cancun we can remove the preimage db entirely. + if provider.cached_storage_settings().use_hashed_state() { + let start_header = provider + .header_by_number(start_block)? + .ok_or_else(|| ProviderError::HeaderNotFound(start_block.into()))?; + + let path = provider.storage_path().join("preimage"); + if !provider.chain_spec().is_cancun_active_at_timestamp(start_header.timestamp()) { + slot_preimages::inject_plain_wipe_slots(&path, provider, &mut state)?; + } else if path.exists() { + // Post-Cancun: no more self-destructs, preimage db is no longer needed. + let _ = std::fs::remove_dir_all(&path); + } + } + // 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. @@ -517,6 +542,8 @@ where }) } + reject_cancun_boundary_unwind(provider, input.checkpoint.block_number, unwind_to)?; + self.ensure_consistency(provider, input.checkpoint.block_number, Some(unwind_to))?; // Unwind account and storage changesets, as well as receipts. @@ -576,6 +603,40 @@ where } } +fn reject_cancun_boundary_unwind( + provider: &Provider, + checkpoint_block: u64, + unwind_to: u64, +) -> Result<(), StageError> +where + Provider: HeaderProvider + ChainSpecProvider, +{ + let checkpoint_header = provider + .header_by_number(checkpoint_block)? + .ok_or_else(|| ProviderError::HeaderNotFound(checkpoint_block.into()))?; + let unwind_to_header = provider + .header_by_number(unwind_to)? + .ok_or_else(|| ProviderError::HeaderNotFound(unwind_to.into()))?; + let checkpoint_is_cancun = + provider.chain_spec().is_cancun_active_at_timestamp(checkpoint_header.timestamp()); + let unwind_to_is_cancun = + provider.chain_spec().is_cancun_active_at_timestamp(unwind_to_header.timestamp()); + if checkpoint_is_cancun && !unwind_to_is_cancun { + return Err(StageError::Fatal( + std::io::Error::other(format!( + "execution unwind across Cancun activation boundary is not allowed: checkpoint \ + block #{checkpoint_block} (ts={}) is Cancun-active but unwind target \ + #{unwind_to} (ts={}) is pre-Cancun", + checkpoint_header.timestamp(), + unwind_to_header.timestamp() + )) + .into(), + )) + } + + Ok(()) +} + fn execution_checkpoint( provider: &StaticFileProvider, start_block: BlockNumber, @@ -687,7 +748,7 @@ mod tests { use alloy_primitives::{address, hex_literal::hex, keccak256, Address, B256, U256}; use alloy_rlp::Decodable; use assert_matches::assert_matches; - use reth_chainspec::ChainSpecBuilder; + use reth_chainspec::{ChainSpecBuilder, EthereumHardfork, ForkCondition}; use reth_db_api::{ models::{metadata::StorageSettings, AccountBeforeTx}, transaction::{DbTx, DbTxMut}, @@ -695,10 +756,11 @@ mod tests { use reth_ethereum_consensus::EthBeaconConsensus; use reth_ethereum_primitives::Block; use reth_evm_ethereum::EthEvmConfig; - use reth_primitives_traits::{Account, Bytecode, SealedBlock, StorageEntry}; + use reth_primitives_traits::{Account, Block as _, Bytecode, SealedBlock, StorageEntry}; use reth_provider::{ - test_utils::create_test_provider_factory, AccountReader, BlockWriter, - DatabaseProviderFactory, ReceiptProvider, StaticFileProviderFactory, + test_utils::{create_test_provider_factory, create_test_provider_factory_with_chain_spec}, + AccountReader, BlockWriter, DatabaseProviderFactory, ReceiptProvider, + StaticFileProviderFactory, }; use reth_prune::PruneModes; use reth_prune_types::{PruneMode, ReceiptsLogPruneConfig}; @@ -1118,6 +1180,75 @@ mod tests { } } + #[test] + fn unwind_from_cancun_to_pre_cancun_is_rejected() { + let chain_spec = Arc::new( + ChainSpecBuilder::mainnet() + .berlin_activated() + .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(15)) + .build(), + ); + let factory = create_test_provider_factory_with_chain_spec(chain_spec); + let provider = factory.database_provider_rw().unwrap(); + + let mut rng = generators::rng(); + let mut genesis = generators::random_block( + &mut rng, + 0, + generators::BlockParams { tx_count: Some(0), ..Default::default() }, + ) + .unseal(); + genesis.header.timestamp = 0; + let genesis = genesis.seal_slow(); + + let mut block_1 = generators::random_block( + &mut rng, + 1, + generators::BlockParams { + parent: Some(genesis.hash()), + tx_count: Some(0), + ..Default::default() + }, + ) + .unseal(); + block_1.header.timestamp = 10; + let block_1 = block_1.seal_slow(); + + let mut block_2 = generators::random_block( + &mut rng, + 2, + generators::BlockParams { + parent: Some(block_1.hash()), + tx_count: Some(0), + ..Default::default() + }, + ) + .unseal(); + block_2.header.timestamp = 20; + let block_2 = block_2.seal_slow(); + + provider.insert_block(&genesis.try_recover().unwrap()).unwrap(); + provider.insert_block(&block_1.try_recover().unwrap()).unwrap(); + provider.insert_block(&block_2.try_recover().unwrap()).unwrap(); + provider + .static_file_provider() + .latest_writer(StaticFileSegment::Headers) + .unwrap() + .commit() + .unwrap(); + + let mut execution_stage = stage(); + let err = execution_stage + .unwind( + &provider, + UnwindInput { checkpoint: StageCheckpoint::new(2), unwind_to: 1, bad_block: None }, + ) + .unwrap_err(); + + assert_matches!(err, StageError::Fatal(_)); + assert!(err.to_string().contains("across Cancun activation boundary")); + } + #[tokio::test] async fn test_selfdestruct() { let test_db = TestStageDB::default(); diff --git a/crates/stages/stages/src/stages/execution/slot_preimages.rs b/crates/stages/stages/src/stages/execution/slot_preimages.rs new file mode 100644 index 0000000000..30cb1e2e69 --- /dev/null +++ b/crates/stages/stages/src/stages/execution/slot_preimages.rs @@ -0,0 +1,219 @@ +use alloy_primitives::{keccak256, map::HashSet, B256}; +use eyre::Context; +use rayon::slice::ParallelSliceMut; +use reth_db::tables; +use reth_db_api::{ + cursor::{DbCursorRO, DbDupCursorRO}, + transaction::DbTx, +}; +use reth_libmdbx::{ + DatabaseFlags, Environment, EnvironmentFlags, Geometry, Mode, SyncMode, WriteFlags, RO, +}; +use reth_provider::{DBProvider, ExecutionOutcome}; +use reth_revm::revm::database::states::RevertToSlot; +use reth_stages_api::StageError; +use std::path::Path; +use tracing::trace; + +/// Separate MDBX environment for storing `keccak256(slot) → slot` preimage mappings. +/// +/// Used only during [`super::ExecutionStage`] for pre-Cancun selfdestruct handling where +/// the original storage slot keys must be recovered from their hashed representation. +/// +/// The database is append-only and not unwound — duplicate inserts are silently skipped. +/// After Cancun (where `SELFDESTRUCT` no longer destroys storage) the database can be pruned. +#[derive(Debug)] +struct SlotPreimages { + env: Environment, +} + +impl SlotPreimages { + /// Opens (or creates) the slot-preimage MDBX environment at the given directory `path`. + /// + /// Uses subdir mode (`no_sub_dir = false`), so MDBX creates `mdbx.dat` / `mdbx.lck` + /// under the directory (e.g. `db/preimage/mdbx.dat`). + fn open(path: &Path) -> eyre::Result { + const GIGABYTE: usize = 1024 * 1024 * 1024; + const TERABYTE: usize = GIGABYTE * 1024; + + let mut builder = Environment::builder(); + builder.set_max_dbs(1); + let os_page_size = page_size::get().clamp(4096, 0x10000); + builder.set_geometry(Geometry { + size: Some(0..(8 * TERABYTE)), + growth_step: Some(4 * GIGABYTE as isize), + shrink_threshold: Some(0), + page_size: Some(reth_libmdbx::PageSize::Set(os_page_size)), + }); + builder.write_map(); + builder.set_flags(EnvironmentFlags { + no_sub_dir: false, + mode: Mode::ReadWrite { sync_mode: SyncMode::Durable }, + ..Default::default() + }); + + let env = builder.open(path).wrap_err_with(|| { + format!("failed to open slot-preimage MDBX env at {}", path.display()) + })?; + + // Ensure the unnamed default DB exists. + { + let tx = env.begin_rw_txn()?; + let _db = tx.create_db(None, DatabaseFlags::empty())?; + tx.commit()?; + } + + trace!(target: "stages::slot_preimages", ?path, "Opened slot-preimage store"); + + Ok(Self { env }) + } + + /// Batch-insert `hashed_slot → plain_slot` preimage entries. + /// + /// Entries must be pre-sorted by key for optimal insert performance. + /// Existing keys are skipped after cursor lookup. + fn insert_preimages(&self, entries: &[(B256, B256)]) -> eyre::Result<()> { + let tx = self.env.begin_rw_txn()?; + let db = tx.open_db(None)?; + let mut cursor = tx.cursor(db.dbi())?; + + for (hashed_slot, plain_slot) in entries { + if cursor.set_key::<[u8; 32], [u8; 32]>(hashed_slot.as_slice())?.is_some() { + continue; + } + cursor.put(hashed_slot.as_slice(), plain_slot.as_slice(), WriteFlags::empty())?; + } + + tx.commit()?; + + trace!(target: "stages::slot_preimages", count = entries.len(), "Inserted slot preimages"); + + Ok(()) + } + + /// Opens a read-only transaction for batch lookups. + /// + /// Reuse the returned [`SlotPreimagesReader`] for multiple `get` calls to avoid + /// the overhead of opening a new RO transaction per lookup. + fn reader(&self) -> eyre::Result { + let tx = self.env.begin_ro_txn()?; + let dbi = tx.open_db(None)?.dbi(); + Ok(SlotPreimagesReader { tx, dbi }) + } +} + +/// Read-only handle for batch slot-preimage lookups within a single MDBX transaction. +struct SlotPreimagesReader { + tx: reth_libmdbx::Transaction, + dbi: reth_libmdbx::ffi::MDBX_dbi, +} + +impl SlotPreimagesReader { + /// Point-lookup of a slot preimage by its keccak256 hash. + fn get(&self, hashed_slot: &B256) -> eyre::Result> { + let result: Option<[u8; 32]> = self.tx.get(self.dbi, hashed_slot.as_ref())?; + Ok(result.map(B256::from)) + } +} + +/// Collects `keccak256(slot) → slot` preimage entries from the bundle state and stores +/// them in the auxiliary preimage database, then rewrites wipe reverts for self-destructed +/// accounts to use plain slot keys instead of relying on the hashed-storage DB walk. +/// +/// This eliminates the need for the changeset writer to read from `HashedStorages` during +/// storage wipes, keeping all changeset keys in plain format. +pub(super) fn inject_plain_wipe_slots( + slot_preimages_path: &Path, + provider: &P, + state: &mut ExecutionOutcome, +) -> Result<(), StageError> { + // Collect preimage entries from bundle state and reverts. + // StorageKey in revm is U256, representing a plain EVM slot index. + let mut preimage_entries = Vec::new(); + let mut seen_hashes = HashSet::new(); + for account in state.bundle.state().values() { + for &slot_key in account.storage.keys() { + let plain = B256::from(slot_key.to_be_bytes()); + let hashed = keccak256(plain); + if seen_hashes.insert(hashed) { + preimage_entries.push((hashed, plain)); + } + } + } + for block_reverts in state.bundle.reverts.iter() { + for (_, revert) in block_reverts { + for &slot_key in revert.storage.keys() { + let plain = B256::from(slot_key.to_be_bytes()); + let hashed = keccak256(plain); + if seen_hashes.insert(hashed) { + preimage_entries.push((hashed, plain)); + } + } + } + } + + // Pre-sort entries by hash key for optimal MDBX insert performance. + preimage_entries.par_sort_unstable_by_key(|(hash, _)| *hash); + + // Lazily open the preimage store and insert entries. + let preimages = SlotPreimages::open(slot_preimages_path).map_err(fatal)?; + + if !preimage_entries.is_empty() { + preimages.insert_preimages(&preimage_entries).map_err(fatal)?; + } + + // Find all wipe reverts (self-destructed accounts) and inject plain slot keys. + + // Open a single RO transaction for all preimage lookups in this batch. + let reader = preimages.reader().map_err(fatal)?; + + for block_reverts in state.bundle.reverts.iter_mut() { + for (address, revert) in block_reverts.iter_mut() { + if !revert.wipe_storage { + continue; + } + + // Walk all hashed storage slots for this account in the DB and look up + // their plain-key preimages. + let addr = *address; + let hashed_address = keccak256(addr); + let mut cursor = provider.tx_ref().cursor_dup_read::()?; + + if let Some((_, entry)) = cursor.seek_exact(hashed_address)? { + inject_preimage_entry(&reader, revert, addr, entry.key, entry.value)?; + while let Some(entry) = cursor.next_dup_val()? { + inject_preimage_entry(&reader, revert, addr, entry.key, entry.value)?; + } + } + } + } + + Ok(()) +} + +/// Looks up the plain-key preimage for a single hashed storage slot and inserts it +/// into the account revert if not already present. +fn inject_preimage_entry( + reader: &SlotPreimagesReader, + revert: &mut reth_revm::revm::database::AccountRevert, + address: alloy_primitives::Address, + hashed_slot: B256, + value: alloy_primitives::U256, +) -> Result<(), StageError> { + let plain_slot = reader.get(&hashed_slot).map_err(fatal)?.ok_or_else(|| { + fatal(eyre::eyre!("missing slot preimage for {hashed_slot:?} (addr={address:?})")) + })?; + + // Convert B256 plain slot to U256 StorageKey for the revert map. + let plain_key = alloy_primitives::U256::from_be_bytes(plain_slot.0); + revert.storage.entry(plain_key).or_insert(RevertToSlot::Some(value)); + Ok(()) +} + +#[inline] +fn fatal(err: E) -> StageError +where + E: Into>, +{ + StageError::Fatal(err.into()) +} diff --git a/crates/stages/stages/src/stages/utils.rs b/crates/stages/stages/src/stages/utils.rs index 0f8392d9bd..2d9e4035f2 100644 --- a/crates/stages/stages/src/stages/utils.rs +++ b/crates/stages/stages/src/stages/utils.rs @@ -208,10 +208,7 @@ where for (idx, changeset_result) in walker.enumerate() { let (BlockNumberAddress((block_number, address)), storage) = changeset_result?; - cache - .entry(AddressStorageKey((address, storage.key.as_b256()))) - .or_default() - .push(block_number); + cache.entry(AddressStorageKey((address, storage.key))).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 77e311b75b..6edb86813e 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, keccak256, Address, Bytes, TxKind, B256, U256}; +use alloy_primitives::{bytes, Address, Bytes, TxKind, B256, U256}; use reth_chainspec::{ChainSpecBuilder, ChainSpecProvider, MAINNET}; use reth_config::config::StageConfig; use reth_consensus::noop::NoopConsensus; @@ -89,11 +89,6 @@ 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() @@ -105,16 +100,6 @@ 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 diff --git a/crates/stages/stages/tests/preimage.rs b/crates/stages/stages/tests/preimage.rs new file mode 100644 index 0000000000..d1faa76acd --- /dev/null +++ b/crates/stages/stages/tests/preimage.rs @@ -0,0 +1,1324 @@ +//! Preimage-specific pipeline tests for storage v2 selfdestruct behavior around Cancun. + +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, keccak256, Address, Bytes, TxKind, B256, U256}; +use reth_chainspec::{ + ChainSpecBuilder, ChainSpecProvider, EthereumHardfork, ForkCondition, MAINNET, +}; +use reth_config::config::StageConfig; +use reth_consensus::noop::NoopConsensus; +use reth_db_common::init::{init_genesis, init_genesis_with_settings}; +use reth_downloaders::{ + bodies::bodies::BodiesDownloaderBuilder, file_client::FileClient, + headers::reverse_headers::ReverseHeadersDownloaderBuilder, +}; +use reth_ethereum_primitives::{Block, BlockBody, Transaction, TransactionSigned}; +use reth_evm::{execute::Executor, ConfigureEvm}; +use reth_evm_ethereum::EthEvmConfig; +use reth_libmdbx::{Environment, EnvironmentFlags, Mode}; +use reth_network_p2p::{ + bodies::downloader::BodyDownloader, + headers::downloader::{HeaderDownloader, SyncTarget}, +}; +use reth_primitives_traits::{ + crypto::secp256k1::public_key_to_address, + proofs::{calculate_receipt_root, calculate_transaction_root}, + RecoveredBlock, SealedBlock, +}; +use reth_provider::{ + test_utils::create_test_provider_factory_with_chain_spec, BlockNumReader, DBProvider, + DatabaseProviderFactory, HeaderProvider, OriginalValuesKnown, StateWriter, StoragePath, +}; +use reth_prune_types::PruneModes; +use reth_revm::database::StateProviderDatabase; +use reth_stages::{ + sets::{ExecutionStages, HashingStages, OnlineStages}, + stages::FinishStage, +}; +use reth_stages_api::{Pipeline, StageSet}; +use reth_static_file::StaticFileProducer; +use reth_storage_api::{StorageChangeSetReader, StorageSettings, StorageSettingsCache}; +use reth_testing_utils::generators::{self, generate_key, sign_tx_with_key_pair}; +use reth_trie::{HashedPostState, KeccakKeyHasher, StateRoot}; +use reth_trie_db::DatabaseStateRoot; +use std::{collections::BTreeMap, path::Path, sync::Arc}; +use tokio::sync::watch; + +type TestProviderFactory = + reth_provider::ProviderFactory; + +const TEST_SELFDESTRUCT_BENEFICIARY: Address = Address::new([0x77; 20]); +const TEST_CREATE2_SALT: B256 = B256::with_last_byte(0x42); + +/// Scenario coverage: +/// 1. Cross-batch pre-Cancun wipe writes plain slot keys into storage changesets. +/// 2. Preimage DB exists and is usable across multiple stage executions. +/// 3. Post-Cancun execution removes the preimage DB directory. +/// +/// Verifies v2 selfdestruct handling across a pre-/post-Cancun boundary. +/// +/// Test flow: +/// 1. Run block 1 (pre-Cancun) and assert the `preimage/` MDBX directory exists and contains +/// `keccak(slot) -> slot` rows for the two written storage slots. +/// 2. Run block 2 (pre-Cancun selfdestruct) and assert storage changesets for the destroyed account +/// contain exactly those two slots as **plain** keys with the expected prior values (`0x2a`, +/// `0x99`). +/// 3. Run block 3 (post-Cancun) and assert `preimage/` is removed, since this auxiliary DB is no +/// longer needed after Cancun semantics are active. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2_selfdestruct_changesets_use_plain_slots() -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + // Build a 3-block scenario: + // - block 1/2 are pre-Cancun (selfdestruct still clears storage) + // - block 3 is post-Cancun (no storage-destroy semantics; preimage DB should be cleaned up) + let scenario = setup_selfdestruct_scenario()?; + + let (pipeline_provider_factory, pipeline_genesis) = + init_v2_pipeline_provider_factory(scenario.chain_spec.clone())?; + + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(vec![scenario.blocks[0].clone()]), + pipeline_genesis, + 1..=1, + 1, + ) + .await?; + + // Phase 1 (pre-Cancun): preimage DB should be created and contain slot preimages. + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 1, "pipeline should sync block 1"); + assert!(provider.cached_storage_settings().storage_v2, "test requires storage.v2 mode"); + + let preimage_path = provider.storage_path().join("preimage"); + let expected_slots = scenario.expected_slots; + assert!(preimage_path.exists(), "preimage dir should exist after first pre-Cancun run"); + assert_preimage_rows(&preimage_path, &expected_slots)?; + + let local_head = + pipeline_provider_factory.sealed_header(1)?.expect("block 1 header should exist"); + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(vec![ + scenario.blocks[0].clone(), + scenario.blocks[1].clone(), + ]), + local_head, + 2..=2, + 2, + ) + .await?; + + // Phase 2 (pre-Cancun selfdestruct): changeset keys for destroyed account must be plain slots. + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 2, "pipeline should sync block 2"); + assert!(preimage_path.exists(), "preimage dir should still exist after second pre-Cancun run"); + assert_preimage_rows(&preimage_path, &expected_slots)?; + assert_destroyed_changeset_entries(&provider, scenario.selfdestruct_contract)?; + + let third_local_head = + pipeline_provider_factory.sealed_header(2)?.expect("block 2 header should exist"); + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(scenario.blocks), + third_local_head, + 3..=3, + 3, + ) + .await?; + + // Phase 3 (post-Cancun): execution path removes the now-unneeded preimage DB directory. + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 3, "pipeline should sync block 3"); + assert!(!preimage_path.exists(), "preimage dir should be removed after post-Cancun execution"); + + Ok(()) +} + +/// Scenario coverage: +/// 1. Single execution batch (`1..=2`) where block 1 writes and block 2 wipes. +/// 2. Block-by-block storage changesets still contain plain keys for wipe entries. +/// +/// Regression coverage for single execution-batch behavior. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2_single_batch_write_then_selfdestruct_changesets_plain_slots( +) -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let scenario = setup_selfdestruct_scenario()?; + let (pipeline_provider_factory, pipeline_genesis) = + init_v2_pipeline_provider_factory(scenario.chain_spec.clone())?; + + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(vec![ + scenario.blocks[0].clone(), + scenario.blocks[1].clone(), + ]), + pipeline_genesis, + 1..=2, + 2, + ) + .await?; + + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 2, "pipeline should sync blocks 1..=2"); + assert_preimage_rows_for_provider(&provider, &scenario.expected_slots)?; + assert_destroyed_changeset_entries(&provider, scenario.selfdestruct_contract)?; + + Ok(()) +} + +/// Scenario coverage: +/// 1. A slot appears only in intermediate reverts (not final bundle state). +/// 2. A later block in the same execution batch wipes the account. +/// 3. Wipe changesets still contain the required plain slot key/value. +/// +/// Covers the edge case where a slot appears in intermediate block reverts but not in the final +/// bundle state, then gets wiped by a later selfdestruct in the same execution batch. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2_single_batch_reverted_slot_then_selfdestruct_changesets_plain_slots( +) -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let scenario = setup_reverted_slot_selfdestruct_scenario()?; + let (pipeline_provider_factory, pipeline_genesis) = + init_v2_pipeline_provider_factory(scenario.chain_spec.clone())?; + + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(scenario.blocks), + pipeline_genesis, + 1..=3, + 3, + ) + .await?; + + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 3, "pipeline should sync blocks 1..=3"); + assert_preimage_rows_for_provider(&provider, &[scenario.expected_slot.0])?; + assert_destroyed_changeset_entries_in_block( + &provider, + 3, + scenario.selfdestruct_contract, + &[scenario.expected_slot], + )?; + + Ok(()) +} + +/// Scenario coverage: +/// 1. Same address is destroyed, recreated via CREATE2, then destroyed again in one execution +/// batch. +/// 2. Both wipe blocks emit plain storage keys for the destroyed account. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2_single_batch_same_address_double_wipe_changesets_plain_slots( +) -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let scenario = setup_same_address_double_wipe_scenario()?; + let (pipeline_provider_factory, pipeline_genesis) = + init_v2_pipeline_provider_factory(scenario.chain_spec.clone())?; + + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(scenario.blocks), + pipeline_genesis, + 1..=6, + 6, + ) + .await?; + + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 6, "pipeline should sync blocks 1..=6"); + let expected_preimages = scenario.expected_slots.map(|(slot, _)| slot); + assert_preimage_rows_for_provider(&provider, &expected_preimages)?; + assert_destroyed_changeset_entries_in_block( + &provider, + 3, + scenario.child_contract, + &scenario.expected_slots, + )?; + assert_destroyed_changeset_entries_in_block( + &provider, + 6, + scenario.child_contract, + &scenario.expected_slots, + )?; + + Ok(()) +} + +/// Scenario coverage: +/// 1. Same address is destroyed at block N. +/// 2. In block N+1, it is recreated and writes new storage in the same block (multi-tx block). +/// 3. At block N+2, it is destroyed again. +/// 4. Both wipe blocks emit plain storage keys for the destroyed account. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2_single_batch_same_address_recreate_and_write_same_block_then_wipe_plain_slots( +) -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let scenario = setup_same_address_recreate_and_write_same_block_then_wipe_scenario()?; + let (pipeline_provider_factory, pipeline_genesis) = + init_v2_pipeline_provider_factory(scenario.chain_spec.clone())?; + + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(scenario.blocks), + pipeline_genesis, + 1..=5, + 5, + ) + .await?; + + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 5, "pipeline should sync blocks 1..=5"); + assert_preimage_rows_for_provider(&provider, &scenario.expected_preimage_slots)?; + assert_destroyed_changeset_entries_in_block( + &provider, + 3, + scenario.child_contract, + &scenario.expected_slots_first_wipe, + )?; + assert_destroyed_changeset_entries_in_block( + &provider, + 5, + scenario.child_contract, + &scenario.expected_slots_second_wipe, + )?; + + Ok(()) +} + +/// Scenario coverage: +/// 1. Intra-block multi-tx net-zero path: tx1 writes slots, tx2 wipes in the same block. +/// 2. Intra-tx net-zero path: one tx writes, restores, and wipes. +/// 3. Net-zero wipe paths emit no storage changeset rows for those accounts. +#[tokio::test(flavor = "multi_thread")] +async fn test_pipeline_v2_single_block_intra_block_and_intra_tx_wipes_use_plain_slots( +) -> eyre::Result<()> { + reth_tracing::init_test_tracing(); + + let scenario = setup_intra_block_and_intra_tx_selfdestruct_scenario()?; + let (pipeline_provider_factory, pipeline_genesis) = + init_v2_pipeline_provider_factory(scenario.chain_spec.clone())?; + + run_pipeline_range( + pipeline_provider_factory.clone(), + create_file_client_from_blocks(scenario.blocks), + pipeline_genesis, + 1..=1, + 1, + ) + .await?; + + let provider = pipeline_provider_factory.provider()?; + assert_eq!(provider.last_block_number()?, 1, "pipeline should sync block 1"); + // Net-zero wipe scenarios do not require any specific preimage rows for changeset emission. + assert_preimage_rows_for_provider(&provider, &[])?; + assert_destroyed_changeset_entries_in_block( + &provider, + 1, + scenario.multi_tx_contract, + &scenario.expected_multi_tx_slots, + )?; + assert_destroyed_changeset_entries_in_block(&provider, 1, scenario.intra_tx_contract, &[])?; + Ok(()) +} + +struct SelfdestructScenario { + chain_spec: Arc, + blocks: Vec>, + selfdestruct_contract: Address, + expected_slots: [B256; 2], +} + +fn setup_selfdestruct_scenario() -> eyre::Result { + let mut rng = generators::rng(); + let key_pair = generate_key(&mut rng); + let signer_address = public_key_to_address(key_pair.public_key()); + let selfdestruct_contract = Address::new([0x66; 20]); + let chain_spec = build_selfdestruct_chain_spec(signer_address, selfdestruct_contract); + let blocks = { + // Build blocks via direct execution first, so each header has a valid state root. + // The pipeline test then replays these exact blocks in phase-separated ranges. + let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); + init_genesis(&provider_factory).expect("init genesis"); + + let genesis = provider_factory.sealed_header(0)?.expect("genesis should exist"); + let evm_config = EthEvmConfig::new(chain_spec.clone()); + let mut blocks = Vec::new(); + let mut parent_hash = genesis.hash(); + let gas_price = INITIAL_BASE_FEE as u128; + + for (block_num, timestamp, nonce, input, to, gas_limit, value) in [ + ( + 1_u64, + 12_u64, + 0_u64, + Bytes::new(), + TxKind::Call(selfdestruct_contract), + 100_000_u64, + U256::ZERO, + ), + ( + 2_u64, + 24_u64, + 1_u64, + Bytes::from(vec![0x01]), + TxKind::Call(selfdestruct_contract), + 100_000_u64, + U256::ZERO, + ), + ( + 3_u64, + 36_u64, + 2_u64, + Bytes::new(), + TxKind::Call(TEST_SELFDESTRUCT_BENEFICIARY), + 21_000_u64, + U256::from(1), + ), + ] { + // Block behavior by timestamp: + // - block 1 (ts=12): writes two storage slots + // - block 2 (ts=24): triggers SELFDESTRUCT (pre-Cancun semantics) + // - block 3 (ts=36): post-Cancun no-op transfer path + let tx = sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce, + gas_limit, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to, + value, + input, + ..Default::default() + }), + ); + let block = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + block_num, + timestamp, + vec![tx], + )?; + parent_hash = block.hash(); + blocks.push(block); + } + + blocks + }; + + Ok(SelfdestructScenario { + chain_spec, + blocks, + selfdestruct_contract, + expected_slots: expected_destroyed_slots(), + }) +} + +struct RevertedSlotSelfdestructScenario { + chain_spec: Arc, + blocks: Vec>, + selfdestruct_contract: Address, + expected_slot: (B256, U256), +} + +struct SameAddressDoubleWipeScenario { + chain_spec: Arc, + blocks: Vec>, + child_contract: Address, + expected_slots: [(B256, U256); 2], +} + +struct SameAddressDifferentSlotsDoubleWipeScenario { + chain_spec: Arc, + blocks: Vec>, + child_contract: Address, + expected_slots_first_wipe: [(B256, U256); 2], + expected_slots_second_wipe: [(B256, U256); 2], + expected_preimage_slots: [B256; 4], +} + +struct IntraBlockAndIntraTxSelfdestructScenario { + chain_spec: Arc, + blocks: Vec>, + multi_tx_contract: Address, + intra_tx_contract: Address, + expected_multi_tx_slots: Vec<(B256, U256)>, +} + +fn setup_reverted_slot_selfdestruct_scenario() -> eyre::Result { + let mut rng = generators::rng(); + let key_pair = generate_key(&mut rng); + let signer_address = public_key_to_address(key_pair.public_key()); + let selfdestruct_contract = Address::new([0x33; 20]); + let slot = B256::with_last_byte(0x03); + let original_value = B256::with_last_byte(0x07); + + let chain_spec = Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(Genesis { + alloc: [ + ( + signer_address, + GenesisAccount { + balance: U256::from(ETH_TO_WEI) * U256::from(1000), + ..Default::default() + }, + ), + ( + selfdestruct_contract, + GenesisAccount { + code: Some(WRITE_RESTORE_OR_SELFDESTRUCT_RUNTIME_CODE), + storage: Some(BTreeMap::from([(slot, original_value)])), + ..Default::default() + }, + ), + ] + .into(), + ..MAINNET.genesis.clone() + }) + .shanghai_activated() + .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(30)) + .build(), + ); + + let blocks = { + let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); + init_genesis(&provider_factory).expect("init genesis"); + + let genesis = provider_factory.sealed_header(0)?.expect("genesis should exist"); + let evm_config = EthEvmConfig::new(chain_spec.clone()); + let mut blocks = Vec::new(); + let mut parent_hash = genesis.hash(); + let gas_price = INITIAL_BASE_FEE as u128; + + for (block_num, timestamp, nonce, value) in [ + (1_u64, 12_u64, 0_u64, U256::ZERO), + (2_u64, 18_u64, 1_u64, U256::from(1_u64)), + (3_u64, 24_u64, 2_u64, U256::from(2_u64)), + ] { + let tx = sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce, + gas_limit: 120_000, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to: TxKind::Call(selfdestruct_contract), + value, + input: Bytes::new(), + ..Default::default() + }), + ); + let block = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + block_num, + timestamp, + vec![tx], + )?; + parent_hash = block.hash(); + blocks.push(block); + } + + blocks + }; + + Ok(RevertedSlotSelfdestructScenario { + chain_spec, + blocks, + selfdestruct_contract, + expected_slot: (slot, U256::from(0x07)), + }) +} + +fn setup_same_address_double_wipe_scenario() -> eyre::Result { + let mut rng = generators::rng(); + let key_pair = generate_key(&mut rng); + let signer_address = public_key_to_address(key_pair.public_key()); + let factory_contract = Address::new([0xaa; 20]); + let child_init = init_code_for_runtime(&WRITE_OR_SELFDESTRUCT_RUNTIME_CODE); + let child_contract = create2_address(factory_contract, TEST_CREATE2_SALT, &child_init); + + let chain_spec = Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(Genesis { + alloc: [ + ( + signer_address, + GenesisAccount { + balance: U256::from(ETH_TO_WEI) * U256::from(1000), + ..Default::default() + }, + ), + ( + factory_contract, + GenesisAccount { + code: Some(CREATE2_FACTORY_RUNTIME_CODE), + ..Default::default() + }, + ), + ] + .into(), + ..MAINNET.genesis.clone() + }) + .shanghai_activated() + .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(30)) + .build(), + ); + + let blocks = { + let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); + init_genesis(&provider_factory).expect("init genesis"); + + let genesis = provider_factory.sealed_header(0)?.expect("genesis should exist"); + let evm_config = EthEvmConfig::new(chain_spec.clone()); + let mut blocks = Vec::new(); + let mut parent_hash = genesis.hash(); + let gas_price = INITIAL_BASE_FEE as u128; + + for (block_num, timestamp, nonce, to, input) in [ + (1_u64, 4_u64, 0_u64, TxKind::Call(factory_contract), child_init.clone()), + (2_u64, 8_u64, 1_u64, TxKind::Call(child_contract), Bytes::new()), + (3_u64, 12_u64, 2_u64, TxKind::Call(child_contract), Bytes::from(vec![0x01])), + (4_u64, 16_u64, 3_u64, TxKind::Call(factory_contract), child_init), + (5_u64, 20_u64, 4_u64, TxKind::Call(child_contract), Bytes::new()), + (6_u64, 24_u64, 5_u64, TxKind::Call(child_contract), Bytes::from(vec![0x01])), + ] { + let tx = sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce, + gas_limit: 400_000, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to, + value: U256::ZERO, + input, + ..Default::default() + }), + ); + let block = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + block_num, + timestamp, + vec![tx], + )?; + parent_hash = block.hash(); + blocks.push(block); + } + + blocks + }; + + Ok(SameAddressDoubleWipeScenario { + chain_spec, + blocks, + child_contract, + expected_slots: [ + (B256::with_last_byte(0x01), U256::from(0x2a)), + (B256::with_last_byte(0x02), U256::from(0x99)), + ], + }) +} + +fn setup_same_address_recreate_and_write_same_block_then_wipe_scenario( +) -> eyre::Result { + let mut rng = generators::rng(); + let key_pair = generate_key(&mut rng); + let signer_address = public_key_to_address(key_pair.public_key()); + let factory_contract = Address::new([0xaa; 20]); + let child_init = init_code_for_runtime(&WRITE_TWO_SLOT_SETS_OR_SELFDESTRUCT_RUNTIME_CODE); + let child_contract = create2_address(factory_contract, TEST_CREATE2_SALT, &child_init); + + let chain_spec = Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(Genesis { + alloc: [ + ( + signer_address, + GenesisAccount { + balance: U256::from(ETH_TO_WEI) * U256::from(1000), + ..Default::default() + }, + ), + ( + factory_contract, + GenesisAccount { + code: Some(CREATE2_FACTORY_RUNTIME_CODE), + ..Default::default() + }, + ), + ] + .into(), + ..MAINNET.genesis.clone() + }) + .shanghai_activated() + .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(30)) + .build(), + ); + + let blocks = { + let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); + init_genesis(&provider_factory).expect("init genesis"); + + let genesis = provider_factory.sealed_header(0)?.expect("genesis should exist"); + let evm_config = EthEvmConfig::new(chain_spec.clone()); + let gas_price = INITIAL_BASE_FEE as u128; + + let mut parent_hash = genesis.hash(); + let mk_tx = |nonce: u64, to: TxKind, value: U256, input: Bytes| { + sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce, + gas_limit: 400_000, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to, + value, + input, + ..Default::default() + }), + ) + }; + + let block1 = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + 1, + 4, + vec![mk_tx(0, TxKind::Call(factory_contract), U256::ZERO, child_init.clone())], + )?; + parent_hash = block1.hash(); + + let block2 = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + 2, + 8, + vec![mk_tx(1, TxKind::Call(child_contract), U256::ZERO, Bytes::new())], + )?; + parent_hash = block2.hash(); + + let block3 = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + 3, + 12, + vec![mk_tx(2, TxKind::Call(child_contract), U256::from(1), Bytes::new())], + )?; + parent_hash = block3.hash(); + + // Recreate and write new slots in the same block (N+1). + let block4 = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + 4, + 16, + vec![ + mk_tx(3, TxKind::Call(factory_contract), U256::ZERO, child_init), + mk_tx(4, TxKind::Call(child_contract), U256::from(2), Bytes::new()), + ], + )?; + parent_hash = block4.hash(); + + let block5 = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + parent_hash, + 5, + 20, + vec![mk_tx(5, TxKind::Call(child_contract), U256::from(1), Bytes::new())], + )?; + + vec![block1, block2, block3, block4, block5] + }; + + Ok(SameAddressDifferentSlotsDoubleWipeScenario { + chain_spec, + blocks, + child_contract, + expected_slots_first_wipe: [ + (B256::with_last_byte(0x01), U256::from(0x2a)), + (B256::with_last_byte(0x02), U256::from(0x99)), + ], + expected_slots_second_wipe: [ + (B256::with_last_byte(0x04), U256::from(0xab)), + (B256::with_last_byte(0x05), U256::from(0xcd)), + ], + expected_preimage_slots: [ + B256::with_last_byte(0x01), + B256::with_last_byte(0x02), + B256::with_last_byte(0x04), + B256::with_last_byte(0x05), + ], + }) +} + +fn setup_intra_block_and_intra_tx_selfdestruct_scenario( +) -> eyre::Result { + let mut rng = generators::rng(); + let key_pair = generate_key(&mut rng); + let signer_address = public_key_to_address(key_pair.public_key()); + let multi_tx_contract = Address::new([0x44; 20]); + let intra_tx_contract = Address::new([0x55; 20]); + + let chain_spec = Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(Genesis { + alloc: [ + ( + signer_address, + GenesisAccount { + balance: U256::from(ETH_TO_WEI) * U256::from(1000), + ..Default::default() + }, + ), + ( + multi_tx_contract, + GenesisAccount { + code: Some(WRITE_OR_SELFDESTRUCT_RUNTIME_CODE), + ..Default::default() + }, + ), + ( + intra_tx_contract, + GenesisAccount { + code: Some(WRITE_RESTORE_THEN_SELFDESTRUCT_RUNTIME_CODE), + ..Default::default() + }, + ), + ] + .into(), + ..MAINNET.genesis.clone() + }) + .shanghai_activated() + .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(30)) + .build(), + ); + + let blocks = { + let provider_factory = create_test_provider_factory_with_chain_spec(chain_spec.clone()); + init_genesis(&provider_factory).expect("init genesis"); + + let genesis = provider_factory.sealed_header(0)?.expect("genesis should exist"); + let evm_config = EthEvmConfig::new(chain_spec.clone()); + let gas_price = INITIAL_BASE_FEE as u128; + + // Single pre-Cancun block with three txs: + // 1) write slots (contract `multi_tx_contract`) + // 2) selfdestruct same contract in same block + // 3) write->restore->selfdestruct in one tx (`intra_tx_contract`) + let txs = vec![ + sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce: 0, + gas_limit: 120_000, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to: TxKind::Call(multi_tx_contract), + value: U256::ZERO, + input: Bytes::new(), + ..Default::default() + }), + ), + sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce: 1, + gas_limit: 120_000, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to: TxKind::Call(multi_tx_contract), + value: U256::ZERO, + input: Bytes::from(vec![0x01]), + ..Default::default() + }), + ), + sign_tx_with_key_pair( + key_pair, + Transaction::Eip1559(TxEip1559 { + chain_id: chain_spec.chain.id(), + nonce: 2, + gas_limit: 120_000, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: 0, + to: TxKind::Call(intra_tx_contract), + value: U256::ZERO, + input: Bytes::new(), + ..Default::default() + }), + ), + ]; + + let block = execute_and_commit_block( + &provider_factory, + &evm_config, + signer_address, + genesis.hash(), + 1, + 12, + txs, + )?; + + vec![block] + }; + + Ok(IntraBlockAndIntraTxSelfdestructScenario { + chain_spec, + blocks, + multi_tx_contract, + intra_tx_contract, + expected_multi_tx_slots: Vec::new(), + }) +} + +fn init_v2_pipeline_provider_factory( + chain_spec: Arc, +) -> eyre::Result<(TestProviderFactory, reth_primitives_traits::SealedHeader
)> { + let pipeline_provider_factory = create_test_provider_factory_with_chain_spec(chain_spec); + init_genesis_with_settings(&pipeline_provider_factory, StorageSettings::v2())?; + pipeline_provider_factory.set_storage_settings_cache(StorageSettings::v2()); + let pipeline_genesis = pipeline_provider_factory + .sealed_header(0)? + .ok_or_else(|| eyre::eyre!("genesis should exist"))?; + Ok((pipeline_provider_factory, pipeline_genesis)) +} + +fn execute_and_commit_block( + provider_factory: &TestProviderFactory, + evm_config: &EthEvmConfig, + signer_address: Address, + parent_hash: B256, + block_num: u64, + timestamp: u64, + transactions: Vec, +) -> eyre::Result> { + let tx_root = calculate_transaction_root(&transactions); + let temp_header = build_execution_header(parent_hash, block_num, timestamp); + let provider = provider_factory.database_provider_rw()?; + let block_with_senders = RecoveredBlock::new_unhashed( + Block::new( + temp_header, + BlockBody { transactions: transactions.clone(), ommers: Vec::new(), withdrawals: None }, + ), + vec![signer_address; transactions.len()], + ); + + let output = { + let state_provider = provider.latest(); + let db = StateProviderDatabase::new(&*state_provider); + let executor = evm_config.batch_executor(db); + executor.execute(&block_with_senders)? + }; + + let gas_used = output.gas_used; + let hashed_state = HashedPostState::from_bundle_state::(output.state.state()); + type TestStateRoot<'a, TX, A> = StateRoot< + reth_trie_db::DatabaseTrieCursorFactory<&'a TX, A>, + reth_trie_db::DatabaseHashedCursorFactory<&'a TX>, + >; + let (state_root, _trie_updates) = reth_trie_db::with_adapter!(provider, |A| { + TestStateRoot::<_, A>::overlay_root_with_updates( + provider.tx_ref(), + &hashed_state.clone().into_sorted(), + ) + })?; + + let receipts: Vec<_> = output.receipts.iter().map(|r| r.with_bloom_ref()).collect(); + let receipts_root = calculate_receipt_root(&receipts); + + let header = Header { + parent_hash, + number: block_num, + state_root, + transactions_root: tx_root, + receipts_root, + gas_limit: 30_000_000, + gas_used, + base_fee_per_gas: Some(INITIAL_BASE_FEE), + timestamp, + parent_beacon_block_root: (timestamp >= 30).then_some(B256::ZERO), + blob_gas_used: (timestamp >= 30).then_some(0), + excess_blob_gas: (timestamp >= 30).then_some(0), + ..Default::default() + }; + + let block: SealedBlock = SealedBlock::seal_parts( + header, + BlockBody { transactions, ommers: Vec::new(), withdrawals: None }, + ); + + let plain_state = output.state.to_plain_state(OriginalValuesKnown::Yes); + provider.write_state_changes(plain_state)?; + provider.write_hashed_state(&hashed_state.into_sorted())?; + provider.commit()?; + + Ok(block) +} + +fn build_selfdestruct_chain_spec( + signer_address: Address, + selfdestruct_contract: Address, +) -> Arc { + let initial_balance = U256::from(ETH_TO_WEI) * U256::from(1000); + + Arc::new( + ChainSpecBuilder::default() + .chain(MAINNET.chain) + .genesis(Genesis { + alloc: [ + ( + signer_address, + GenesisAccount { balance: initial_balance, ..Default::default() }, + ), + ( + selfdestruct_contract, + GenesisAccount { + code: Some(WRITE_OR_SELFDESTRUCT_RUNTIME_CODE), + ..Default::default() + }, + ), + ] + .into(), + ..MAINNET.genesis.clone() + }) + .shanghai_activated() + .with_fork(EthereumHardfork::Cancun, ForkCondition::Timestamp(30)) + .build(), + ) +} + +fn build_execution_header(parent_hash: B256, number: u64, timestamp: u64) -> Header { + Header { + parent_hash, + number, + gas_limit: 30_000_000, + base_fee_per_gas: Some(INITIAL_BASE_FEE), + timestamp, + parent_beacon_block_root: (timestamp >= 30).then_some(B256::ZERO), + blob_gas_used: (timestamp >= 30).then_some(0), + excess_blob_gas: (timestamp >= 30).then_some(0), + ..Default::default() + } +} + +const fn expected_destroyed_slots() -> [B256; 2] { + [B256::with_last_byte(0x01), B256::with_last_byte(0x02)] +} + +fn assert_destroyed_changeset_entries

( + provider: &P, + selfdestruct_contract: Address, +) -> eyre::Result<()> +where + P: StorageChangeSetReader, +{ + let expected = [ + (B256::with_last_byte(0x01), U256::from(0x2a)), + (B256::with_last_byte(0x02), U256::from(0x99)), + ]; + assert_destroyed_changeset_entries_in_block(provider, 2, selfdestruct_contract, &expected) +} + +fn assert_destroyed_changeset_entries_in_block

( + provider: &P, + block: u64, + selfdestruct_contract: Address, + expected: &[(B256, U256)], +) -> eyre::Result<()> +where + P: StorageChangeSetReader, +{ + let storage_changesets = provider.storage_changesets_range(block..=block)?; + let destroyed_entries: Vec<_> = storage_changesets + .into_iter() + .filter_map(|(key, entry)| { + (key.address() == selfdestruct_contract).then_some((entry.key, entry.value)) + }) + .collect(); + + assert_eq!( + destroyed_entries.len(), + expected.len(), + "expected exactly {} storage changeset entries for destroyed account at block {}", + expected.len(), + block + ); + + for (slot, _) in &destroyed_entries { + assert_ne!(*slot, keccak256(*slot), "storage changeset key should be plain (not hashed)"); + } + + for pair in expected { + assert!( + destroyed_entries.contains(pair), + "missing expected storage changeset entry for destroyed account: {:?}", + pair + ); + } + + Ok(()) +} + +fn create_file_client_from_blocks(blocks: Vec>) -> Arc> { + Arc::new(FileClient::from_blocks(blocks)) +} + +fn build_pipeline_without_history( + provider_factory: TestProviderFactory, + header_downloader: H, + body_downloader: B, + max_block: u64, + tip: B256, +) -> Pipeline +where + H: HeaderDownloader

+ 'static, + B: BodyDownloader + 'static, +{ + let consensus = NoopConsensus::arc(); + let stages_config = StageConfig::default(); + let evm_config = EthEvmConfig::new(provider_factory.chain_spec()); + + let (tip_tx, tip_rx) = watch::channel(B256::ZERO); + let static_file_producer = + StaticFileProducer::new(provider_factory.clone(), PruneModes::default()); + + let stages = OnlineStages::new( + provider_factory.clone(), + tip_rx, + header_downloader, + body_downloader, + stages_config.clone(), + None, + ) + .builder() + .add_set(ExecutionStages::new( + evm_config, + consensus, + stages_config, + PruneModes::default().sender_recovery, + )) + .add_set(HashingStages::default()) + .add_stage(FinishStage::default()); + + let pipeline = Pipeline::builder() + .with_tip_sender(tip_tx) + .with_max_block(max_block) + .with_fail_on_unwind(true) + .add_stages(stages) + .build(provider_factory, static_file_producer); + pipeline.set_tip(tip); + pipeline +} + +async fn run_pipeline_range( + provider_factory: TestProviderFactory, + file_client: Arc>, + local_head: reth_primitives_traits::SealedHeader
, + download_range: std::ops::RangeInclusive, + max_block: u64, +) -> eyre::Result<()> { + // Run a narrow range intentionally so the test can assert per-phase behavior. + let tip = file_client.tip().expect("tip"); + let consensus = NoopConsensus::arc(); + let stages_config = StageConfig::default(); + let runtime = reth_tasks::Runtime::test(); + + let mut header_downloader = ReverseHeadersDownloaderBuilder::new(stages_config.headers) + .build(file_client.clone(), consensus.clone()) + .into_task_with(&runtime); + header_downloader.update_local_head(local_head); + header_downloader.update_sync_target(SyncTarget::Tip(tip)); + + let mut body_downloader = BodiesDownloaderBuilder::new(stages_config.bodies) + .build(file_client, consensus, provider_factory.clone()) + .into_task_with(&runtime); + body_downloader.set_download_range(download_range).expect("set download range"); + + let pipeline = build_pipeline_without_history( + provider_factory, + header_downloader, + body_downloader, + max_block, + tip, + ); + let (_pipeline, result) = pipeline.run_as_fut(None).await; + result?; + Ok(()) +} + +/// Builds tiny runtime bytecode that branches on calldata: +/// - empty calldata: writes two known slots and stops +/// - non-empty calldata: selfdestructs to `beneficiary` and stops +/// +/// The known slot/value pairs are used for deterministic assertions in changesets and preimages. +const WRITE_OR_SELFDESTRUCT_RUNTIME_CODE: Bytes = bytes!( + "3615601c57" // CALLDATASIZE; ISZERO; PUSH1 0x1c; JUMPI + "737777777777777777777777777777777777777777" // PUSH20 beneficiary + "ff00" // SELFDESTRUCT; STOP + "5b" // JUMPDEST (0x1c) + "602a600155" // SSTORE(1, 0x2a) + "6099600255" // SSTORE(2, 0x99) + "00" // STOP +); + +/// Builds tiny runtime bytecode with three callvalue-based paths: +/// - `msg.value == 0`: write slot-set A (`(1, 0x2a)`, `(2, 0x99)`) +/// - `msg.value == 2`: write slot-set B (`(4, 0xab)`, `(5, 0xcd)`) +/// - `msg.value == 1`: selfdestruct to `beneficiary` +/// +/// Used by the recreate-and-write-in-same-block scenario to ensure first and second wipes +/// restore different slot sets for the same address. +const WRITE_TWO_SLOT_SETS_OR_SELFDESTRUCT_RUNTIME_CODE: Bytes = bytes!( + "34600114602557" // if callvalue == 1 jump selfdestruct + "34600214601957" // if callvalue == 2 jump write slot-set B + "602a600155" // write slot-set A: SSTORE(1, 0x2a) + "6099600255" // write slot-set A: SSTORE(2, 0x99) + "00" // STOP + "5b" // JUMPDEST (0x19) + "60ab600455" // write slot-set B: SSTORE(4, 0xab) + "60cd600555" // write slot-set B: SSTORE(5, 0xcd) + "00" // STOP + "5b" // JUMPDEST (0x25) + "737777777777777777777777777777777777777777" // PUSH20 beneficiary + "ff00" // SELFDESTRUCT; STOP +); + +/// Builds tiny runtime bytecode with three value-based paths: +/// - `msg.value == 0`: SSTORE(3, 0x2b) +/// - `msg.value == 1`: SSTORE(3, 0x07) +/// - `msg.value == 2`: SELFDESTRUCT to `beneficiary` +const WRITE_RESTORE_OR_SELFDESTRUCT_RUNTIME_CODE: Bytes = bytes!( + "34600214601b57" // if callvalue == 2 jump selfdestruct + "34600114601457" // if callvalue == 1 jump restore + "602b60035500" // default: SSTORE(3, 0x2b); STOP + "5b" // JUMPDEST (0x14) + "600760035500" // restore: SSTORE(3, 0x07); STOP + "5b" // JUMPDEST (0x1b) + "737777777777777777777777777777777777777777" // PUSH20 beneficiary + "ff00" // SELFDESTRUCT; STOP +); + +/// Builds tiny runtime bytecode that performs all actions in one call: +/// SSTORE(3, 0x2b) -> SSTORE(3, 0x07) -> SELFDESTRUCT. +const WRITE_RESTORE_THEN_SELFDESTRUCT_RUNTIME_CODE: Bytes = bytes!( + "602b600355" // SSTORE(3, 0x2b) + "6007600355" // SSTORE(3, 0x07) + "737777777777777777777777777777777777777777" // PUSH20 beneficiary + "ff00" // SELFDESTRUCT; STOP +); + +/// Converts contract runtime bytecode into init code that returns the runtime. +fn init_code_for_runtime(runtime: &Bytes) -> Bytes { + let len = u8::try_from(runtime.len()).expect("runtime too large for PUSH1 init-code helper"); + let mut init = Vec::with_capacity(12 + runtime.len()); + init.extend_from_slice(&[ + 0x60, len, // PUSH1 runtime_len + 0x60, 0x0c, // PUSH1 runtime_offset + 0x60, 0x00, // PUSH1 mem_offset + 0x39, // CODECOPY + 0x60, len, // PUSH1 runtime_len + 0x60, 0x00, // PUSH1 mem_offset + 0xf3, // RETURN + ]); + init.extend_from_slice(runtime.as_ref()); + init.into() +} + +/// Runtime bytecode for a minimal CREATE2 factory: +/// - calldata is treated as init code +/// - deploys with fixed `salt` +const CREATE2_FACTORY_RUNTIME_CODE: Bytes = bytes!( + "366000600037" // CALLDATACOPY(0, 0, calldatasize) + "7f0000000000000000000000000000000000000000000000000000000000000042" // PUSH32 salt + "3660006000f500" // CREATE2(0, 0, calldatasize, salt); STOP +); + +fn create2_address(factory: Address, salt: B256, init_code: &Bytes) -> Address { + let init_hash = keccak256(init_code.as_ref()); + let mut preimage = [0_u8; 85]; + preimage[0] = 0xff; + preimage[1..21].copy_from_slice(factory.as_slice()); + preimage[21..53].copy_from_slice(salt.as_slice()); + preimage[53..85].copy_from_slice(init_hash.as_slice()); + + let hash = keccak256(preimage); + Address::from_slice(&hash.as_slice()[12..]) +} + +fn assert_preimage_rows(preimage_path: &Path, slots: &[B256]) -> eyre::Result<()> { + let mut builder = Environment::builder(); + builder.set_max_dbs(1); + builder.set_flags(EnvironmentFlags { + no_sub_dir: false, + mode: Mode::ReadOnly, + ..Default::default() + }); + + let env = builder.open(preimage_path)?; + let tx = env.begin_ro_txn()?; + let db = tx.open_db(None)?; + + for slot in slots { + let hashed = keccak256(*slot); + let found: Option<[u8; 32]> = tx.get(db.dbi(), hashed.as_slice())?; + assert_eq!( + found.map(B256::from), + Some(*slot), + "missing/invalid preimage row for slot {:?}", + slot + ); + } + + Ok(()) +} + +fn assert_preimage_rows_for_provider

(provider: &P, slots: &[B256]) -> eyre::Result<()> +where + P: StoragePath, +{ + let preimage_path = provider.storage_path().join("preimage"); + assert!( + preimage_path.exists(), + "preimage dir should exist for pre-Cancun execution at {}", + preimage_path.display() + ); + assert_preimage_rows(&preimage_path, slots) +} diff --git a/crates/storage/db-api/src/database.rs b/crates/storage/db-api/src/database.rs index 1f8e3e125a..71403505a1 100644 --- a/crates/storage/db-api/src/database.rs +++ b/crates/storage/db-api/src/database.rs @@ -3,7 +3,7 @@ use crate::{ transaction::{DbTx, DbTxMut}, DatabaseError, }; -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, path::PathBuf, sync::Arc}; /// Main Database trait that can open read-only and read-write transactions. /// @@ -22,6 +22,9 @@ pub trait Database: Send + Sync + Debug { #[track_caller] fn tx_mut(&self) -> Result; + /// Returns the path to the database directory. + fn path(&self) -> PathBuf; + /// Takes a function and passes a read-only transaction into it, making sure it's closed in the /// end of the execution. fn view(&self, f: F) -> Result @@ -62,6 +65,10 @@ impl Database for Arc { fn tx_mut(&self) -> Result { ::tx_mut(self) } + + fn path(&self) -> PathBuf { + ::path(self) + } } impl Database for &DB { @@ -75,4 +82,8 @@ impl Database for &DB { fn tx_mut(&self) -> Result { ::tx_mut(self) } + + fn path(&self) -> PathBuf { + ::path(self) + } } diff --git a/crates/storage/db-api/src/mock.rs b/crates/storage/db-api/src/mock.rs index 78a2aec1e1..324f3cddac 100644 --- a/crates/storage/db-api/src/mock.rs +++ b/crates/storage/db-api/src/mock.rs @@ -16,7 +16,7 @@ use crate::{ DatabaseError, }; use core::ops::Bound; -use std::{collections::BTreeMap, ops::RangeBounds}; +use std::{collections::BTreeMap, ops::RangeBounds, path::PathBuf}; /// Mock database implementation for testing and development. /// @@ -50,6 +50,10 @@ impl Database for DatabaseMock { fn tx_mut(&self) -> Result { Ok(TxMock::default()) } + + fn path(&self) -> PathBuf { + PathBuf::default() + } } impl DatabaseMetrics for DatabaseMock {} diff --git a/crates/storage/db/src/implementation/mdbx/mod.rs b/crates/storage/db/src/implementation/mdbx/mod.rs index 484b99e517..325a7918de 100644 --- a/crates/storage/db/src/implementation/mdbx/mod.rs +++ b/crates/storage/db/src/implementation/mdbx/mod.rs @@ -25,7 +25,7 @@ use reth_tracing::tracing::error; use std::{ collections::HashMap, ops::{Deref, Range}, - path::Path, + path::{Path, PathBuf}, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -244,6 +244,8 @@ impl DatabaseArguments { pub struct DatabaseEnv { /// Libmdbx-sys environment. inner: Environment, + /// Path to the database directory. + path: PathBuf, /// Opened DBIs for reuse. /// Important: Do not manually close these DBIs, like via `mdbx_dbi_close`. /// More generally, do not dynamically create, re-open, or drop tables at @@ -277,6 +279,10 @@ impl Database for DatabaseEnv { ) .map_err(|e| DatabaseError::InitTx(e.into())) } + + fn path(&self) -> PathBuf { + self.path.clone() + } } impl DatabaseMetrics for DatabaseEnv { @@ -508,6 +514,7 @@ impl DatabaseEnv { let env = Self { inner: inner_env.open(path).map_err(|e| DatabaseError::Open(e.into()))?, + path: path.to_path_buf(), dbis: Arc::default(), metrics: None, _lock_file, diff --git a/crates/storage/db/src/lib.rs b/crates/storage/db/src/lib.rs index 160dc7b08d..97a33b138c 100644 --- a/crates/storage/db/src/lib.rs +++ b/crates/storage/db/src/lib.rs @@ -140,6 +140,10 @@ pub mod test_utils { fn tx_mut(&self) -> Result { self.db().tx_mut() } + + fn path(&self) -> std::path::PathBuf { + self.db().path() + } } impl DatabaseMetrics for TempDatabase { @@ -241,7 +245,7 @@ mod tests { // Test that TempDatabase properly cleans up its directory when dropped let temp_path = { let db = crate::test_utils::create_test_rw_db(); - let path = db.path().to_path_buf(); + let path = db.path(); assert!(path.exists(), "Database directory should exist while TempDatabase is alive"); path // TempDatabase dropped here diff --git a/crates/storage/provider/src/changeset_walker.rs b/crates/storage/provider/src/changeset_walker.rs index ba4fe4811c..5eb521e3a7 100644 --- a/crates/storage/provider/src/changeset_walker.rs +++ b/crates/storage/provider/src/changeset_walker.rs @@ -5,7 +5,8 @@ use crate::ProviderResult; use alloy_primitives::BlockNumber; use reth_db::models::AccountBeforeTx; use reth_db_api::models::BlockNumberAddress; -use reth_storage_api::{ChangeSetReader, ChangesetEntry, StorageChangeSetReader}; +use reth_primitives_traits::StorageEntry; +use reth_storage_api::{ChangeSetReader, StorageChangeSetReader}; use std::ops::{Bound, RangeBounds}; /// Iterator that walks account changesets from static files in a block range. @@ -109,7 +110,7 @@ pub struct StaticFileStorageChangesetWalker

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

Iterator for StaticFileStorageChangesetWalker

where P: StorageChangeSetReader, { - type Item = ProviderResult<(BlockNumberAddress, ChangesetEntry)>; + type Item = ProviderResult<(BlockNumberAddress, StorageEntry)>; 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 63de5fd845..a9cf4c38f4 100644 --- a/crates/storage/provider/src/providers/blockchain_provider.rs +++ b/crates/storage/provider/src/providers/blockchain_provider.rs @@ -23,13 +23,11 @@ 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}; +use reth_primitives_traits::{Account, RecoveredBlock, SealedHeader, StorageEntry}; use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; -use reth_storage_api::{ - BlockBodyIndicesProvider, ChangesetEntry, NodePrimitivesProvider, StorageChangeSetReader, -}; +use reth_storage_api::{BlockBodyIndicesProvider, NodePrimitivesProvider, StorageChangeSetReader}; use reth_storage_errors::provider::ProviderResult; use reth_trie::{HashedPostState, KeccakKeyHasher}; use revm_database::BundleState; @@ -715,7 +713,7 @@ impl StorageChangeSetReader for BlockchainProvider { fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { self.consistent_provider()?.storage_changeset(block_number) } @@ -724,14 +722,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 1a63457894..b0dd4f5524 100644 --- a/crates/storage/provider/src/providers/consistent.rs +++ b/crates/storage/provider/src/providers/consistent.rs @@ -21,16 +21,13 @@ 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, StorageSlotKey, -}; +use reth_primitives_traits::{Account, BlockBody, RecoveredBlock, SealedHeader, StorageEntry}; use reth_prune_types::{PruneCheckpoint, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; use reth_storage_api::{ - BlockBodyIndicesProvider, ChangesetEntry, DatabaseProviderFactory, NodePrimitivesProvider, - StateProvider, StateProviderBox, StorageChangeSetReader, StorageSettingsCache, - TryIntoHistoricalStateProvider, + BlockBodyIndicesProvider, DatabaseProviderFactory, NodePrimitivesProvider, StateProvider, + StateProviderBox, StorageChangeSetReader, TryIntoHistoricalStateProvider, }; use reth_storage_errors::provider::ProviderResult; use revm_database::states::PlainStorageRevert; @@ -220,13 +217,13 @@ impl ConsistentProvider { /// 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. + /// Storage changeset keys are always plain (unhashed). Current values are read via + /// [`StateProvider::storage`], which handles hashing internally when `use_hashed_state` is + /// enabled. fn populate_bundle_state( &self, account_changeset: Vec<(u64, AccountBeforeTx)>, - storage_changeset: Vec<(BlockNumberAddress, ChangesetEntry)>, + storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>, block_range_end: BlockNumber, ) -> ProviderResult<(BundleStateInit, RevertsInit)> { let mut state: BundleStateInit = HashMap::default(); @@ -263,16 +260,10 @@ impl ConsistentProvider { }; // match storage. - match account_state.2.entry(old_storage.key.as_b256()) { + match account_state.2.entry(old_storage.key) { hash_map::Entry::Vacant(entry) => { - 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(), - }; + let new_storage_value = + state_provider.storage(address, old_storage.key)?.unwrap_or_default(); entry.insert((old_storage.value, new_storage_value)); } hash_map::Entry::Occupied(mut entry) => { @@ -286,7 +277,7 @@ impl ConsistentProvider { .entry(address) .or_default() .1 - .push(StorageEntry::from(old_storage)); + .push(old_storage); } Ok((state, reverts)) @@ -1312,8 +1303,7 @@ impl StorageChangeSetReader for ConsistentProvider { fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { - let use_hashed = self.storage_provider.cached_storage_settings().use_hashed_state(); + ) -> ProviderResult> { if let Some(state) = self.head_block.as_ref().and_then(|b| b.block_on_chain(block_number.into())) { @@ -1329,10 +1319,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); + let plain_key = B256::from(key.to_be_bytes()); ( BlockNumberAddress((block_number, revert.address)), - ChangesetEntry { key: tagged_key, value: value.to_previous_value() }, + StorageEntry { key: plain_key, value: value.to_previous_value() }, ) }) }) @@ -1367,8 +1357,7 @@ impl StorageChangeSetReader for ConsistentProvider { block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult> { - let use_hashed = self.storage_provider.cached_storage_settings().use_hashed_state(); + ) -> ProviderResult> { if let Some(state) = self.head_block.as_ref().and_then(|b| b.block_on_chain(block_number.into())) { @@ -1387,9 +1376,9 @@ impl StorageChangeSetReader for ConsistentProvider { return None } revert.storage_revert.into_iter().find_map(|(key, value)| { - let tagged_key = StorageSlotKey::from_u256(key).to_changeset(use_hashed); - (tagged_key.as_b256() == storage_key).then(|| ChangesetEntry { - key: tagged_key, + let plain_key = B256::from(key.to_be_bytes()); + (plain_key == storage_key).then(|| StorageEntry { + key: plain_key, value: value.to_previous_value(), }) }) @@ -1415,14 +1404,12 @@ 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; @@ -1440,14 +1427,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); + let plain_key = B256::from(key.to_be_bytes()); ( BlockNumberAddress((state.number(), revert.address)), - ChangesetEntry { - key: tagged_key, - value: value.to_previous_value(), - }, + StorageEntry { key: plain_key, value: value.to_previous_value() }, ) }) }); @@ -2084,178 +2067,6 @@ 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; @@ -2337,104 +2148,6 @@ mod tests { 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; @@ -2521,8 +2234,8 @@ mod tests { 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(); + let db_key = db_changeset[0].1.key; + let mem_key = mem_changeset[0].1.key; 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"); @@ -2534,101 +2247,6 @@ mod tests { 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; @@ -2715,7 +2333,7 @@ mod tests { 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(); + let keys: Vec = all_changesets.iter().map(|(_, entry)| entry.key).collect(); assert_eq!( keys[0], keys[1], diff --git a/crates/storage/provider/src/providers/database/mod.rs b/crates/storage/provider/src/providers/database/mod.rs index 5ff4ddc7aa..8e8178ffd2 100644 --- a/crates/storage/provider/src/providers/database/mod.rs +++ b/crates/storage/provider/src/providers/database/mod.rs @@ -119,6 +119,7 @@ impl ProviderFactory { rocksdb_provider.clone(), ChangesetCache::new(), runtime.clone(), + db.path(), ) .storage_settings()? .unwrap_or(legacy_settings); @@ -246,6 +247,7 @@ impl ProviderFactory { self.rocksdb_provider.clone(), self.changeset_cache.clone(), self.runtime.clone(), + self.db.path(), )) } @@ -265,6 +267,7 @@ impl ProviderFactory { self.rocksdb_provider.clone(), self.changeset_cache.clone(), self.runtime.clone(), + self.db.path(), ))) } @@ -285,6 +288,7 @@ impl ProviderFactory { self.rocksdb_provider.clone(), self.changeset_cache.clone(), self.runtime.clone(), + self.db.path(), )) } diff --git a/crates/storage/provider/src/providers/database/provider.rs b/crates/storage/provider/src/providers/database/provider.rs index 562c7fc2a5..1f05d55b79 100644 --- a/crates/storage/provider/src/providers/database/provider.rs +++ b/crates/storage/provider/src/providers/database/provider.rs @@ -52,7 +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, FastInstant as Instant, RecoveredBlock, - SealedHeader, StorageEntry, StorageSlotKey, + SealedHeader, StorageEntry, }; use reth_prune_types::{ PruneCheckpoint, PruneMode, PruneModes, PruneSegment, MINIMUM_UNWIND_SAFE_DISTANCE, @@ -60,8 +60,8 @@ use reth_prune_types::{ use reth_stages_types::{StageCheckpoint, StageId}; use reth_static_file_types::StaticFileSegment; use reth_storage_api::{ - BlockBodyIndicesProvider, BlockBodyReader, ChangesetEntry, MetadataProvider, MetadataWriter, - NodePrimitivesProvider, StateProvider, StateWriteConfig, StorageChangeSetReader, + BlockBodyIndicesProvider, BlockBodyReader, MetadataProvider, MetadataWriter, + NodePrimitivesProvider, StateProvider, StateWriteConfig, StorageChangeSetReader, StoragePath, StorageSettingsCache, TryIntoHistoricalStateProvider, WriteStateInput, }; use reth_storage_errors::provider::{ProviderResult, StaticFileWriterError}; @@ -78,6 +78,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, fmt::Debug, ops::{Deref, DerefMut, Range, RangeBounds, RangeInclusive}, + path::PathBuf, sync::Arc, }; use tracing::{debug, instrument, trace}; @@ -201,6 +202,8 @@ pub struct DatabaseProvider { changeset_cache: ChangesetCache, /// Task runtime for spawning parallel I/O work. runtime: reth_tasks::Runtime, + /// Path to the database directory. + db_path: PathBuf, /// Pending `RocksDB` batches to be committed at provider commit time. #[cfg_attr(not(all(unix, feature = "rocksdb")), allow(dead_code))] pending_rocksdb_batches: PendingRocksDBBatches, @@ -358,6 +361,7 @@ impl DatabaseProvider { rocksdb_provider: RocksDBProvider, changeset_cache: ChangesetCache, runtime: reth_tasks::Runtime, + db_path: PathBuf, commit_order: CommitOrder, ) -> Self { Self { @@ -370,6 +374,7 @@ impl DatabaseProvider { rocksdb_provider, changeset_cache, runtime, + db_path, pending_rocksdb_batches: Default::default(), commit_order, minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE, @@ -389,6 +394,7 @@ impl DatabaseProvider { rocksdb_provider: RocksDBProvider, changeset_cache: ChangesetCache, runtime: reth_tasks::Runtime, + db_path: PathBuf, ) -> Self { Self::new_rw_inner( tx, @@ -400,6 +406,7 @@ impl DatabaseProvider { rocksdb_provider, changeset_cache, runtime, + db_path, CommitOrder::Normal, ) } @@ -416,6 +423,7 @@ impl DatabaseProvider { rocksdb_provider: RocksDBProvider, changeset_cache: ChangesetCache, runtime: reth_tasks::Runtime, + db_path: PathBuf, ) -> Self { Self::new_rw_inner( tx, @@ -427,6 +435,7 @@ impl DatabaseProvider { rocksdb_provider, changeset_cache, runtime, + db_path, CommitOrder::Unwind, ) } @@ -984,6 +993,7 @@ impl DatabaseProvider { rocksdb_provider: RocksDBProvider, changeset_cache: ChangesetCache, runtime: reth_tasks::Runtime, + db_path: PathBuf, ) -> Self { Self { tx, @@ -995,6 +1005,7 @@ impl DatabaseProvider { rocksdb_provider, changeset_cache, runtime, + db_path, pending_rocksdb_batches: Default::default(), commit_order: CommitOrder::Normal, minimum_pruning_distance: MINIMUM_UNWIND_SAFE_DISTANCE, @@ -1198,7 +1209,7 @@ impl DatabaseProvider { fn populate_bundle_state( &self, account_changeset: Vec<(u64, AccountBeforeTx)>, - storage_changeset: Vec<(BlockNumberAddress, ChangesetEntry)>, + storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>, plain_accounts_cursor: &mut A, plain_storage_cursor: &mut S, ) -> ProviderResult<(BundleStateInit, RevertsInit)> @@ -1248,12 +1259,11 @@ impl DatabaseProvider { }; // match storage. - let storage_key = old_storage.key.as_b256(); - match account_state.2.entry(storage_key) { + match account_state.2.entry(old_storage.key) { hash_map::Entry::Vacant(entry) => { let new_storage = plain_storage_cursor - .seek_by_key_subkey(address, storage_key)? - .filter(|storage| storage.key == storage_key) + .seek_by_key_subkey(address, old_storage.key)? + .filter(|storage| storage.key == old_storage.key) .unwrap_or_default(); entry.insert((old_storage.value, new_storage.value)); } @@ -1268,20 +1278,20 @@ impl DatabaseProvider { .entry(address) .or_default() .1 - .push(old_storage.into_storage_entry()); + .push(old_storage); } 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. + /// `HashedAccounts`/`HashedStorages`. Addresses and storage keys are hashed via `keccak256` + /// for DB lookups. The output `BundleStateInit`/`RevertsInit` structures remain keyed by + /// plain address and plain storage key. fn populate_bundle_state_hashed( &self, account_changeset: Vec<(u64, AccountBeforeTx)>, - storage_changeset: Vec<(BlockNumberAddress, ChangesetEntry)>, + storage_changeset: Vec<(BlockNumberAddress, StorageEntry)>, hashed_accounts_cursor: &mut impl DbCursorRO, hashed_storage_cursor: &mut impl DbDupCursorRO, ) -> ProviderResult<(BundleStateInit, RevertsInit)> { @@ -1318,13 +1328,14 @@ impl DatabaseProvider { hash_map::Entry::Occupied(entry) => entry.into_mut(), }; - let storage_key = old_storage.key.as_b256(); - match account_state.2.entry(storage_key) { + // Storage keys in changesets are plain; hash them for HashedStorages lookup. + let hashed_storage_key = keccak256(old_storage.key); + match account_state.2.entry(old_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) + .seek_by_key_subkey(hashed_address, hashed_storage_key)? + .filter(|storage| storage.key == hashed_storage_key) .unwrap_or_default(); entry.insert((old_storage.value, new_storage.value)); } @@ -1339,7 +1350,7 @@ impl DatabaseProvider { .entry(address) .or_default() .1 - .push(old_storage.into_storage_entry()); + .push(old_storage); } Ok((state, reverts)) @@ -1499,7 +1510,7 @@ impl StorageChangeSetReader for DatabaseProvider fn storage_changeset( &self, block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { if self.cached_storage_settings().storage_v2 { self.static_file_provider.storage_changeset(block_number) } else { @@ -1510,13 +1521,7 @@ impl StorageChangeSetReader for DatabaseProvider .walk_range(storage_range)? .map(|r| { let (bna, entry) = r?; - Ok(( - bna, - ChangesetEntry { - key: StorageSlotKey::plain(entry.key), - value: entry.value, - }, - )) + Ok((bna, entry)) }) .collect() } @@ -1527,7 +1532,7 @@ impl StorageChangeSetReader for DatabaseProvider block_number: BlockNumber, address: Address, storage_key: B256, - ) -> ProviderResult> { + ) -> ProviderResult> { if self.cached_storage_settings().storage_v2 { self.static_file_provider.get_storage_before_block(block_number, address, storage_key) } else { @@ -1535,18 +1540,14 @@ impl StorageChangeSetReader for DatabaseProvider .tx .cursor_dup_read::()? .seek_by_key_subkey(BlockNumberAddress((block_number, address)), storage_key)? - .filter(|entry| entry.key == storage_key) - .map(|entry| ChangesetEntry { - key: StorageSlotKey::plain(entry.key), - value: entry.value, - })) + .filter(|entry| entry.key == storage_key)) } } fn storage_changesets_range( &self, range: impl RangeBounds, - ) -> ProviderResult> { + ) -> ProviderResult> { if self.cached_storage_settings().storage_v2 { self.static_file_provider.storage_changesets_range(range) } else { @@ -1555,13 +1556,7 @@ impl StorageChangeSetReader for DatabaseProvider .walk_range(BlockNumberAddressRange::from(range))? .map(|r| { let (bna, entry) = r?; - Ok(( - bna, - ChangesetEntry { - key: StorageSlotKey::plain(entry.key), - value: entry.value, - }, - )) + Ok((bna, entry)) }) .collect() } @@ -2306,7 +2301,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.as_b256()); + accounts.entry(address).or_default().insert(storage_entry.key); Ok(accounts) }, ) @@ -2336,7 +2331,7 @@ impl StorageReader for DatabaseProvider BTreeMap::new(), |mut storages: BTreeMap<(Address, B256), Vec>, (index, storage)| { storages - .entry((index.address(), storage.key.as_b256())) + .entry((index.address(), storage.key)) .or_default() .push(index.block_number()); Ok(storages) @@ -2495,15 +2490,11 @@ 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; @@ -2516,9 +2507,7 @@ impl StateWriter for PlainStorageRevert { address, wiped, storage_revert } in storage_changes { let mut storage = storage_revert .into_iter() - .map(|(k, v)| { - (StorageSlotKey::from_u256(k).to_changeset_key(use_hashed_state), v) - }) + .map(|(k, v)| (B256::from(k.to_be_bytes()), v)) .collect::>(); // sort storage slots by key. storage.par_sort_unstable_by_key(|a| a.0); @@ -2527,29 +2516,13 @@ impl StateWriter // storage state has to be taken from the database and written to storage // history. See [StorageWipe::Primary] for more details. // - // 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 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)? { + 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)) @@ -2597,9 +2570,6 @@ impl StateWriter changes.storage.par_sort_by_key(|a| a.address); changes.contracts.par_sort_by_key(|a| a.0); - // 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"); @@ -2740,12 +2710,7 @@ impl StateWriter changeset_writer.prune_storage_changesets(block)?; changesets } else { - self.take::(storage_range)? - .into_iter() - .map(|(k, v)| { - (k, ChangesetEntry { key: StorageSlotKey::plain(v.key), value: v.value }) - }) - .collect() + self.take::(storage_range)?.into_iter().collect() }; let account_changeset = if self.cached_storage_settings().storage_v2 { let changesets = self.account_changesets_range(range)?; @@ -2781,11 +2746,12 @@ impl StateWriter for (storage_key, (old_storage_value, _new_storage_value)) in storage { let hashed_address = keccak256(address); + let hashed_storage_key = keccak256(storage_key); let storage_entry = - StorageEntry { key: *storage_key, value: *old_storage_value }; + StorageEntry { key: hashed_storage_key, value: *old_storage_value }; if hashed_storage_cursor - .seek_by_key_subkey(hashed_address, *storage_key)? - .filter(|s| s.key == *storage_key) + .seek_by_key_subkey(hashed_address, hashed_storage_key)? + .filter(|s| s.key == hashed_storage_key) .is_some() { hashed_storage_cursor.delete_current()? @@ -2898,12 +2864,7 @@ impl StateWriter changeset_writer.prune_storage_changesets(block)?; changesets } else { - self.take::(storage_range)? - .into_iter() - .map(|(k, v)| { - (k, ChangesetEntry { key: StorageSlotKey::plain(v.key), value: v.value }) - }) - .collect() + self.take::(storage_range)?.into_iter().collect() }; // if there are static files for this segment, prune them. @@ -2950,11 +2911,12 @@ impl StateWriter for (storage_key, (old_storage_value, _new_storage_value)) in storage { let hashed_address = keccak256(address); + let hashed_storage_key = keccak256(storage_key); let storage_entry = - StorageEntry { key: *storage_key, value: *old_storage_value }; + StorageEntry { key: hashed_storage_key, value: *old_storage_value }; if hashed_storage_cursor - .seek_by_key_subkey(hashed_address, *storage_key)? - .filter(|s| s.key == *storage_key) + .seek_by_key_subkey(hashed_address, hashed_storage_key)? + .filter(|s| s.key == hashed_storage_key) .is_some() { hashed_storage_cursor.delete_current()? @@ -3212,13 +3174,13 @@ 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)| { - let hashed_key = storage_entry.key.to_hashed(); + let hashed_key = keccak256(storage_entry.key); (keccak256(address), hashed_key, storage_entry.value) }) .collect::>(); @@ -3361,13 +3323,11 @@ 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.as_b256(), bn) - }) + .map(|(BlockNumberAddress((bn, address)), storage)| (address, storage.key, bn)) .collect::>(); storage_changesets.sort_by_key(|(address, key, _)| (*address, *key)); @@ -3699,7 +3659,6 @@ 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(); @@ -3708,8 +3667,7 @@ 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 = - StorageSlotKey::from_u256(*storage_key).to_changeset_key(use_hashed); + let key = B256::from(storage_key.to_be_bytes()); storage_transitions.entry((*address, key)).or_default().push(block_number); } } @@ -3944,6 +3902,12 @@ impl StorageSettingsCache for DatabaseProvider { } } +impl StoragePath for DatabaseProvider { + fn storage_path(&self) -> PathBuf { + self.db_path.clone() + } +} + #[cfg(test)] mod tests { use super::*; @@ -4616,7 +4580,8 @@ mod tests { let address = Address::random(); let hashed_address = keccak256(address); - let slot_key_already_hashed = B256::random(); + let plain_slot = B256::random(); + let hashed_slot = keccak256(plain_slot); let current_value = U256::from(100); let old_value = U256::from(42); @@ -4626,32 +4591,26 @@ mod tests { .tx .cursor_dup_write::() .unwrap() - .upsert( - hashed_address, - &StorageEntry { key: slot_key_already_hashed, value: current_value }, - ) + .upsert(hashed_address, &StorageEntry { key: hashed_slot, value: current_value }) .unwrap(); let changesets = vec![( BlockNumberAddress((1, address)), - ChangesetEntry { - key: StorageSlotKey::Hashed(slot_key_already_hashed), - value: old_value, - }, + StorageEntry { key: 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(&slot_key_already_hashed)); + 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, slot_key_already_hashed) + .seek_by_key_subkey(hashed_address, hashed_slot) .unwrap() .expect("entry should exist"); - assert_eq!(entry.key, slot_key_already_hashed); + assert_eq!(entry.key, hashed_slot); assert_eq!(entry.value, old_value); } @@ -4849,7 +4808,7 @@ mod tests { let changesets = vec![( BlockNumberAddress((1, address)), - ChangesetEntry { key: StorageSlotKey::Plain(plain_slot), value: old_value }, + StorageEntry { key: plain_slot, value: old_value }, )]; let result = provider_rw.unwind_storage_hashing(changesets.into_iter()).unwrap(); @@ -4964,7 +4923,7 @@ mod tests { 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); + assert_eq!(storage_cs[0].1.key, slot_key); let account_cs = sf.account_block_changeset(1).unwrap(); assert!(!account_cs.is_empty()); @@ -5170,8 +5129,8 @@ mod tests { for (_, entry) in &storage_cs { assert!( - entry.key.is_hashed(), - "v2: static file storage changeset should have hashed slot keys" + entry.key != keccak256(entry.key), + "v2: static file storage changeset should have plain slot keys" ); } } @@ -5192,10 +5151,7 @@ mod tests { 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(); + let shards = rocksdb.storage_history_shards(address, slot_key).unwrap(); assert!( !shards.is_empty(), "v2: RocksDB StoragesHistory missing for block {block_num} acct {acct_idx} slot {s}" @@ -5377,11 +5333,7 @@ mod tests { 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" - ); + assert_eq!(storage_cs[0].1.key, slot_key, "v2: changeset key should be plain"); provider_rw.remove_state_above(0).unwrap(); @@ -5414,78 +5366,6 @@ mod tests { 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() { @@ -5494,15 +5374,14 @@ mod tests { 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.append_storage_history_shard(address, slot_key, vec![3u64, 7, 10]).unwrap(); batch.commit().unwrap(); - let shards = rocksdb.storage_history_shards(address, hashed_slot).unwrap(); + let shards = rocksdb.storage_history_shards(address, slot_key).unwrap(); assert!(!shards.is_empty(), "history should be written to rocksdb"); } @@ -5511,11 +5390,11 @@ mod tests { let changesets = vec![ ( BlockNumberAddress((7, address)), - ChangesetEntry { key: StorageSlotKey::Hashed(hashed_slot), value: U256::from(5) }, + StorageEntry { key: slot_key, value: U256::from(5) }, ), ( BlockNumberAddress((10, address)), - ChangesetEntry { key: StorageSlotKey::Hashed(hashed_slot), value: U256::from(8) }, + StorageEntry { key: slot_key, value: U256::from(8) }, ), ]; @@ -5525,7 +5404,7 @@ mod tests { provider_rw.commit().unwrap(); let rocksdb = factory.rocksdb_provider(); - let shards = rocksdb.storage_history_shards(address, hashed_slot).unwrap(); + let shards = rocksdb.storage_history_shards(address, slot_key).unwrap(); assert!( !shards.is_empty(), diff --git a/crates/storage/provider/src/providers/rocksdb/invariants.rs b/crates/storage/provider/src/providers/rocksdb/invariants.rs index 1d64a84a8b..d3568e18a5 100644 --- a/crates/storage/provider/src/providers/rocksdb/invariants.rs +++ b/crates/storage/provider/src/providers/rocksdb/invariants.rs @@ -317,10 +317,7 @@ impl RocksDBProvider { let unique_keys: HashSet<_> = changesets .into_iter() - .map(|(block_addr, entry)| { - // entry.key is a hashed storage key - (block_addr.address(), entry.key.as_b256(), checkpoint + 1) - }) + .map(|(block_addr, entry)| (block_addr.address(), entry.key, 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 574473e49a..a60023ea9c 100644 --- a/crates/storage/provider/src/providers/rocksdb/provider.rs +++ b/crates/storage/provider/src/providers/rocksdb/provider.rs @@ -2,7 +2,6 @@ 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, }; @@ -1345,9 +1344,8 @@ impl RocksDBProvider { for revert in storage_block_reverts { for (slot, _) in revert.storage_revert { let plain_key = B256::new(slot.to_be_bytes()); - let key = keccak256(plain_key); storage_history - .entry((revert.address, key)) + .entry((revert.address, plain_key)) .or_default() .push(block_number); } diff --git a/crates/storage/provider/src/providers/state/historical.rs b/crates/storage/provider/src/providers/state/historical.rs index e9f0cfb395..dd757122fe 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, StorageSlotKey}; +use reth_primitives_traits::{Account, Bytecode}; use reth_storage_api::{ BlockNumReader, BytecodeReader, DBProvider, NodePrimitivesProvider, StateProofProvider, StorageChangeSetReader, StorageRootProvider, StorageSettingsCache, @@ -169,10 +169,12 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block } /// Lookup a storage key in the `StoragesHistory` table using `EitherReader`. + /// + /// `lookup_key` is always a plain (unhashed) storage key. pub fn storage_history_lookup( &self, address: Address, - storage_key: StorageSlotKey, + lookup_key: B256, ) -> ProviderResult where Provider: StorageSettingsCache + RocksDBProviderFactory + NodePrimitivesProvider, @@ -181,16 +183,6 @@ 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( @@ -205,27 +197,16 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block /// 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. + /// `lookup_key` is always a plain (unhashed) storage key. fn storage_by_lookup_key( &self, address: Address, - storage_key: StorageSlotKey, + lookup_key: B256, ) -> 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)? { + match self.storage_history_lookup(address, lookup_key)? { HistoryInfo::NotYetWritten => Ok(None), HistoryInfo::InChangeset(changeset_block_number) => self .provider @@ -240,11 +221,12 @@ impl<'b, Provider: DBProvider + ChangeSetReader + StorageChangeSetReader + Block HistoryInfo::InPlainState | HistoryInfo::MaybeInPlainState => { if self.provider.cached_storage_settings().use_hashed_state() { let hashed_address = alloy_primitives::keccak256(address); + let hashed_slot = alloy_primitives::keccak256(lookup_key); Ok(self .tx() .cursor_dup_read::()? - .seek_by_key_subkey(hashed_address, lookup_key)? - .filter(|entry| entry.key == lookup_key) + .seek_by_key_subkey(hashed_address, hashed_slot)? + .filter(|entry| entry.key == hashed_slot) .map(|entry| entry.value) .or(Some(StorageValue::ZERO))) } else { @@ -572,18 +554,7 @@ impl< address: Address, storage_key: StorageKey, ) -> ProviderResult> { - 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)) + self.storage_by_lookup_key(address, storage_key) } } @@ -776,7 +747,7 @@ mod tests { transaction::{DbTx, DbTxMut}, BlockNumberList, }; - use reth_primitives_traits::{Account, StorageEntry, StorageSlotKey}; + use reth_primitives_traits::{Account, StorageEntry}; use reth_storage_api::{ BlockHashReader, BlockNumReader, ChangeSetReader, DBProvider, DatabaseProviderFactory, NodePrimitivesProvider, StorageChangeSetReader, StorageSettingsCache, @@ -1031,7 +1002,7 @@ mod tests { Err(ProviderError::StateAtBlockPruned(number)) if number == provider.block_number )); assert!(matches!( - provider.storage_history_lookup(ADDRESS, StorageSlotKey::plain(STORAGE)), + provider.storage_history_lookup(ADDRESS, STORAGE), Err(ProviderError::StateAtBlockPruned(number)) if number == provider.block_number )); @@ -1050,7 +1021,7 @@ mod tests { Ok(HistoryInfo::MaybeInPlainState) )); assert!(matches!( - provider.storage_history_lookup(ADDRESS, StorageSlotKey::plain(STORAGE)), + provider.storage_history_lookup(ADDRESS, STORAGE), Ok(HistoryInfo::MaybeInPlainState) )); @@ -1069,7 +1040,7 @@ mod tests { Ok(HistoryInfo::MaybeInPlainState) )); assert!(matches!( - provider.storage_history_lookup(ADDRESS, StorageSlotKey::plain(STORAGE)), + provider.storage_history_lookup(ADDRESS, STORAGE), Ok(HistoryInfo::MaybeInPlainState) )); } @@ -1333,105 +1304,4 @@ 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 eb4a96471e..5f8caea547 100644 --- a/crates/storage/provider/src/providers/state/latest.rs +++ b/crates/storage/provider/src/providers/state/latest.rs @@ -263,18 +263,6 @@ impl StateProvide 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) - } - } } impl BytecodeReader @@ -318,7 +306,6 @@ mod tests { }; use reth_primitives_traits::StorageEntry; use reth_storage_api::StorageSettingsCache; - use reth_storage_errors::provider::ProviderError; const fn assert_state_provider() {} #[expect(dead_code)] @@ -434,51 +421,4 @@ mod tests { 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/static_file/manager.rs b/crates/storage/provider/src/providers/static_file/manager.rs index 3b3409fb8f..914d2faaa1 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, StorageSlotKey, + SignedTransaction, StorageEntry, }; 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, ChangesetEntry, DBProvider, PruneCheckpointReader, + BlockBodyIndicesProvider, ChangeSetReader, 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: StorageSlotKey::from_u256(key).to_hashed(), + key: B256::from(key.to_be_bytes()), 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,10 +2538,7 @@ 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 = ChangesetEntry { - key: StorageSlotKey::hashed(change.key), - value: change.value, - }; + let entry = StorageEntry { key: change.key, value: change.value }; changeset.push((block_address, entry)); } } @@ -2556,7 +2553,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, @@ -2605,10 +2602,7 @@ impl StorageChangeSetReader for StaticFileProvider { .get_one::(low.into())? .filter(|change| change.address == address && change.key == storage_key) { - return Ok(Some(ChangesetEntry { - key: StorageSlotKey::hashed(change.key), - value: change.value, - })); + return Ok(Some(StorageEntry { key: change.key, value: change.value })); } Ok(None) @@ -2617,7 +2611,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 3a1673a4f7..50cd204df2 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.as_b256(), test_key); + assert_eq!(entry.key, 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.as_b256(), test_key); + assert_eq!(entry.key, 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.as_b256(), other_key); + assert_eq!(entry.key, 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.as_b256(), keys[0]); + assert_eq!(entry.key, 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.as_b256(), keys[num_slots - 1]); + assert_eq!(entry.key, 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.as_b256(), keys[mid]); + assert_eq!(entry.key, 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.as_b256(), keys[i]); + assert_eq!(result.unwrap().key, keys[i]); } } } diff --git a/crates/storage/provider/src/test_utils/mock.rs b/crates/storage/provider/src/test_utils/mock.rs index a4ed876489..c8c6e80e21 100644 --- a/crates/storage/provider/src/test_utils/mock.rs +++ b/crates/storage/provider/src/test_utils/mock.rs @@ -28,12 +28,12 @@ use reth_ethereum_primitives::EthPrimitives; use reth_execution_types::ExecutionOutcome; use reth_primitives_traits::{ Account, Block, BlockBody, Bytecode, GotExpected, NodePrimitives, RecoveredBlock, SealedHeader, - SignerRecoverable, + SignerRecoverable, StorageEntry, }; use reth_prune_types::{PruneCheckpoint, PruneModes, PruneSegment}; use reth_stages_types::{StageCheckpoint, StageId}; use reth_storage_api::{ - BlockBodyIndicesProvider, BytecodeReader, ChangesetEntry, DBProvider, DatabaseProviderFactory, + BlockBodyIndicesProvider, BytecodeReader, DBProvider, DatabaseProviderFactory, HashedPostStateProvider, NodePrimitivesProvider, StageCheckpointReader, StateProofProvider, StorageChangeSetReader, StorageRootProvider, StorageSettingsCache, }; @@ -883,14 +883,6 @@ 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 @@ -1029,7 +1021,7 @@ impl StorageChangeSetReader fn storage_changeset( &self, _block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult> { Ok(Vec::default()) } @@ -1038,14 +1030,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/rpc-provider/src/lib.rs b/crates/storage/rpc-provider/src/lib.rs index 651dddf91c..74cc772059 100644 --- a/crates/storage/rpc-provider/src/lib.rs +++ b/crates/storage/rpc-provider/src/lib.rs @@ -1091,14 +1091,6 @@ 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 196cff8929..39f3a538d7 100644 --- a/crates/storage/storage-api/src/hashing.rs +++ b/crates/storage/storage-api/src/hashing.rs @@ -1,4 +1,3 @@ -use crate::ChangesetEntry; use alloc::collections::{BTreeMap, BTreeSet}; use alloy_primitives::{map::B256Map, Address, BlockNumber, B256}; use auto_impl::auto_impl; @@ -48,7 +47,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 e9bc3db5f2..d3a21c183d 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/lib.rs b/crates/storage/storage-api/src/lib.rs index 8c69e2090f..7b2f36cf1f 100644 --- a/crates/storage/storage-api/src/lib.rs +++ b/crates/storage/storage-api/src/lib.rs @@ -98,6 +98,8 @@ pub use header_sync_gap::HeaderSyncGapProvider; #[cfg(feature = "db-api")] pub mod metadata; +#[cfg(all(feature = "db-api", feature = "std"))] +pub use metadata::StoragePath; #[cfg(feature = "db-api")] pub use metadata::{MetadataProvider, MetadataWriter, StorageSettingsCache}; #[cfg(feature = "db-api")] diff --git a/crates/storage/storage-api/src/macros.rs b/crates/storage/storage-api/src/macros.rs index 42d8fbfe5f..a299c529b8 100644 --- a/crates/storage/storage-api/src/macros.rs +++ b/crates/storage/storage-api/src/macros.rs @@ -41,7 +41,6 @@ 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/metadata.rs b/crates/storage/storage-api/src/metadata.rs index f8fcfb883f..927d262fca 100644 --- a/crates/storage/storage-api/src/metadata.rs +++ b/crates/storage/storage-api/src/metadata.rs @@ -56,3 +56,10 @@ pub trait StorageSettingsCache: Send { /// [`MetadataWriter::write_storage_settings`] fn set_storage_settings_cache(&self, settings: StorageSettings); } + +/// Trait for accessing the database directory path. +#[cfg(feature = "std")] +pub trait StoragePath: Send { + /// Returns the path to the database directory (e.g. `/db`). + fn storage_path(&self) -> std::path::PathBuf; +} diff --git a/crates/storage/storage-api/src/noop.rs b/crates/storage/storage-api/src/noop.rs index 728c91db93..ee51b2458b 100644 --- a/crates/storage/storage-api/src/noop.rs +++ b/crates/storage/storage-api/src/noop.rs @@ -413,7 +413,9 @@ impl StorageChangeSetReader for NoopProvider< fn storage_changeset( &self, _block_number: BlockNumber, - ) -> ProviderResult> { + ) -> ProviderResult< + Vec<(reth_db_api::models::BlockNumberAddress, reth_primitives_traits::StorageEntry)>, + > { Ok(Vec::default()) } @@ -422,14 +424,16 @@ 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> { + ) -> ProviderResult< + Vec<(reth_db_api::models::BlockNumberAddress, reth_primitives_traits::StorageEntry)>, + > { Ok(Vec::default()) } @@ -538,14 +542,6 @@ 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 { diff --git a/crates/storage/storage-api/src/state.rs b/crates/storage/storage-api/src/state.rs index 10d86e1490..789e3dfdce 100644 --- a/crates/storage/storage-api/src/state.rs +++ b/crates/storage/storage-api/src/state.rs @@ -41,27 +41,12 @@ 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 993f3fdca0..30ed267177 100644 --- a/crates/storage/storage-api/src/storage.rs +++ b/crates/storage/storage-api/src/storage.rs @@ -2,38 +2,11 @@ use alloc::{ collections::{BTreeMap, BTreeSet}, vec::Vec, }; -use alloy_primitives::{Address, BlockNumber, B256, U256}; +use alloy_primitives::{Address, BlockNumber, B256}; use core::ops::RangeInclusive; -use reth_primitives_traits::{StorageEntry, StorageSlotKey}; +use reth_primitives_traits::StorageEntry; 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(&, Arc, Box)] pub trait StorageReader: Send { @@ -64,35 +37,26 @@ pub trait StorageReader: Send { #[auto_impl::auto_impl(&, Arc, 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 core::ops::RangeBounds, - ) -> ProviderResult>; + ) -> ProviderResult>; /// Get the total count of all storage changes. fn storage_changeset_count(&self) -> ProviderResult; @@ -109,7 +73,7 @@ pub trait StorageChangeSetReader: Send { .into_iter() .map(|(block_address, entry)| reth_db_models::StorageBeforeTx { address: block_address.address(), - key: entry.key.as_b256(), + key: entry.key, value: entry.value, }) .collect() diff --git a/crates/trie/db/src/prefix_set.rs b/crates/trie/db/src/prefix_set.rs index 440e9027cb..6d5bc1bdf0 100644 --- a/crates/trie/db/src/prefix_set.rs +++ b/crates/trie/db/src/prefix_set.rs @@ -19,11 +19,6 @@ use reth_trie::{ /// Load prefix sets using a provider that implements [`ChangeSetReader`]. This function can read /// changesets from both static files and database. -/// -/// 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, @@ -61,7 +56,7 @@ where storage_prefix_sets .entry(hashed_address) .or_default() - .insert(Nibbles::unpack(storage_entry.key.to_hashed())); + .insert(Nibbles::unpack(keccak256(storage_entry.key))); } Ok(TriePrefixSets { diff --git a/crates/trie/db/src/state.rs b/crates/trie/db/src/state.rs index e690412809..daf42b1a8c 100644 --- a/crates/trie/db/src/state.rs +++ b/crates/trie/db/src/state.rs @@ -145,11 +145,6 @@ 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. - /// - /// 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, @@ -269,9 +264,6 @@ impl<'a, TX: DbTx, A: crate::TrieTableAdapter> 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 @@ -291,8 +283,7 @@ impl DatabaseHashedPostState for HashedPostStateSorted { /// /// - Reads the first occurrence of each changed account/storage slot in the range. /// - 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). + /// - Storage keys are always plain and are hashed via `keccak256`. /// - Returns keys already ordered for trie iteration. #[instrument(target = "trie::db", skip(provider), fields(range))] fn from_reverts( @@ -333,12 +324,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.as_b256())) { + if seen_storage_keys.insert((address, storage.key)) { let hashed_address = keccak256(address); storages .entry(hashed_address) .or_default() - .push((storage.key.to_hashed(), storage.value)); + .push((keccak256(storage.key), storage.value)); } } } @@ -598,7 +589,7 @@ mod tests { .append_storage_changeset( vec![StorageBeforeTx { address: address1, - key: hashed_slot2, + key: plain_slot2, value: U256::from(200), }], 1, @@ -608,7 +599,7 @@ mod tests { .append_storage_changeset( vec![StorageBeforeTx { address: address1, - key: hashed_slot1, + key: plain_slot1, value: U256::from(100), }], 2, @@ -618,7 +609,7 @@ mod tests { .append_storage_changeset( vec![StorageBeforeTx { address: address1, - key: hashed_slot1, + key: plain_slot1, value: U256::from(999), }], 3, diff --git a/crates/trie/db/src/storage.rs b/crates/trie/db/src/storage.rs index d705850615..e82f883655 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 = storage_change.key.to_hashed(); + let hashed_slot = keccak256(storage_change.key); if let hash_map::Entry::Vacant(entry) = storage.storage.entry(hashed_slot) { entry.insert(storage_change.value); } @@ -213,9 +213,9 @@ mod tests { &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) }]), + (1, vec![StorageBeforeTx { address, key: plain_slot1, value: U256::from(10) }]), + (2, vec![StorageBeforeTx { address, key: plain_slot2, value: U256::from(20) }]), + (3, vec![StorageBeforeTx { address, key: plain_slot1, value: U256::from(999) }]), ], );