From b0c340d0554201b1c92531c42ffd50274e7e2b4b Mon Sep 17 00:00:00 2001 From: James Prestwich Date: Mon, 16 Dec 2024 13:27:22 -0500 Subject: [PATCH] `ConcurrentState` (#66) * feat: concurrentstate * doc: add a line * chore: integrate into trevm * chore: make it a feature * lint: clipppppppy * fix: add some missing muts and update docs * feat: builder port * refactor: rename * feat: clean up sync requirement --- Cargo.toml | 5 + src/db/builder.rs | 181 ++++++++++++++++++++ src/db/cache_state.rs | 175 +++++++++++++++++++ src/db/mod.rs | 43 +++++ src/db/sync_state.rs | 378 ++++++++++++++++++++++++++++++++++++++++++ src/evm.rs | 12 +- src/lib.rs | 4 + 7 files changed, 792 insertions(+), 6 deletions(-) create mode 100644 src/db/builder.rs create mode 100644 src/db/cache_state.rs create mode 100644 src/db/mod.rs create mode 100644 src/db/sync_state.rs diff --git a/Cargo.toml b/Cargo.toml index 0a5440b..ce16426 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,8 @@ revm = { version = "18.0.0", default-features = false } zenith-types = { version = "0.10", optional = true } +dashmap = { version = "6.1.0", optional = true } + [dev-dependencies] alloy-rlp = { version = "0.3", default-features = false } revm = { version = "18.0.0", features = [ @@ -56,6 +58,7 @@ tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] } [features] default = [ "std", + "concurrent-db", "revm/std", "revm/c-kzg", "revm/blst", @@ -65,6 +68,8 @@ default = [ std = ["revm/std", "alloy/std", "alloy-rlp/std", "alloy-primitives/std", "alloy-sol-types/std", "dep:zenith-types"] +concurrent-db = ["std", "dep:dashmap"] + test-utils = ["revm/test-utils", "revm/std", "revm/serde-json", "revm/alloydb"] secp256k1 = ["revm/secp256k1"] diff --git a/src/db/builder.rs b/src/db/builder.rs new file mode 100644 index 0000000..89db4a7 --- /dev/null +++ b/src/db/builder.rs @@ -0,0 +1,181 @@ +use crate::db::ConcurrentState; +use revm::{ + db::{ + states::{BundleState, TransitionState}, + EmptyDB, + }, + primitives::{db::DatabaseRef, B256}, +}; +use std::collections::BTreeMap; + +use super::{ConcurrentCacheState, ConcurrentStateInfo}; + +/// Allows building of State and initializing it with different options. +#[derive(Clone, Debug)] +pub struct ConcurrentStateBuilder { + /// Database that we use to fetch data from. + database: Db, + /// Enabled state clear flag that is introduced in Spurious Dragon hardfork. + /// Default is true as spurious dragon happened long time ago. + with_state_clear: bool, + /// if there is prestate that we want to use. + /// This would mean that we have additional state layer between evm and disk/database. + with_bundle_prestate: Option, + /// This will initialize cache to this state. + with_cache_prestate: Option, + /// Do we want to create reverts and update bundle state. + /// Default is false. + with_bundle_update: bool, + /// Do we want to merge transitions in background. + /// This will allows evm to continue executing. + /// Default is false. + with_background_transition_merge: bool, + /// If we want to set different block hashes + with_block_hashes: BTreeMap, +} + +impl ConcurrentStateBuilder { + /// Create a new builder with an empty database. + /// + /// If you want to instantiate it with a specific database, use + /// [`new_with_database`](Self::new_with_database). + pub fn new() -> Self { + Self::default() + } +} + +impl Default for ConcurrentStateBuilder { + fn default() -> Self { + Self::new_with_database(Db::default()) + } +} + +impl ConcurrentStateBuilder { + /// Create a new builder with the given database. + pub const fn new_with_database(database: Db) -> Self { + Self { + database, + with_state_clear: true, + with_cache_prestate: None, + with_bundle_prestate: None, + with_bundle_update: false, + with_background_transition_merge: false, + with_block_hashes: BTreeMap::new(), + } + } + + /// Set the database. + pub fn with_database( + self, + database: ODb, + ) -> ConcurrentStateBuilder { + // cast to the different database, + // Note that we return different type depending of the database NewDBError. + ConcurrentStateBuilder { + with_state_clear: self.with_state_clear, + database, + with_cache_prestate: self.with_cache_prestate, + with_bundle_prestate: self.with_bundle_prestate, + with_bundle_update: self.with_bundle_update, + with_background_transition_merge: self.with_background_transition_merge, + with_block_hashes: self.with_block_hashes, + } + } + + /// Alias for [`Self::with_database`], for revm compat reasons. + pub fn with_database_ref( + self, + database: ODb, + ) -> ConcurrentStateBuilder { + self.with_database(database) + } + + /// By default state clear flag is enabled but for initial sync on mainnet + /// we want to disable it so proper consensus changes are in place. + pub fn without_state_clear(self) -> Self { + Self { with_state_clear: false, ..self } + } + + /// Allows setting prestate that is going to be used for execution. + /// This bundle state will act as additional layer of cache. + /// and State after not finding data inside StateCache will try to find it inside BundleState. + /// + /// On update Bundle state will be changed and updated. + pub fn with_bundle_prestate(self, bundle: BundleState) -> Self { + Self { with_bundle_prestate: Some(bundle), ..self } + } + + /// Make transitions and update bundle state. + /// + /// This is needed option if we want to create reverts + /// and getting output of changed states. + pub fn with_bundle_update(self) -> Self { + Self { with_bundle_update: true, ..self } + } + + /// It will use different cache for the state. If set, it will ignore bundle prestate. + /// and will ignore `without_state_clear` flag as cache contains its own state_clear flag. + /// + /// This is useful for testing. + pub fn with_cached_prestate(self, cache: impl Into) -> Self { + Self { with_cache_prestate: Some(cache.into()), ..self } + } + + /// Starts the thread that will take transitions and do merge to the bundle state + /// in the background. + pub fn with_background_transition_merge(self) -> Self { + Self { with_background_transition_merge: true, ..self } + } + + /// Add block hashes to the state. + pub fn with_block_hashes(self, block_hashes: BTreeMap) -> Self { + Self { with_block_hashes: block_hashes, ..self } + } + + /// Build the concurrent state. + pub fn build(mut self) -> ConcurrentState { + let use_preloaded_bundle = if self.with_cache_prestate.is_some() { + self.with_bundle_prestate = None; + false + } else { + self.with_bundle_prestate.is_some() + }; + ConcurrentState::new( + self.database, + ConcurrentStateInfo { + cache: self + .with_cache_prestate + .unwrap_or_else(|| ConcurrentCacheState::new(self.with_state_clear)), + transition_state: self.with_bundle_update.then(TransitionState::default), + bundle_state: self.with_bundle_prestate.unwrap_or_default(), + use_preloaded_bundle, + block_hashes: self.with_block_hashes.into(), + }, + ) + } +} + +// Some code above and documentation is adapted from the revm crate, and is +// reproduced here under the terms of the MIT license. +// +// MIT License +// +// Copyright (c) 2021-2024 draganrakita +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. diff --git a/src/db/cache_state.rs b/src/db/cache_state.rs new file mode 100644 index 0000000..01d1f50 --- /dev/null +++ b/src/db/cache_state.rs @@ -0,0 +1,175 @@ +//! The majority of this code has been reproduced from revm. + +use alloy_primitives::{Address, B256}; +use dashmap::DashMap; +use revm::{ + db::states::{plain_account::PlainStorage, CacheAccount}, + primitives::{Account, AccountInfo, Bytecode, EvmState}, + CacheState, TransitionAccount, +}; + +/// A concurrent version of [`revm::db::CacheState`]. +/// +/// Most of the code for this has been reproduced from revm. +#[derive(Debug, Clone)] +pub struct ConcurrentCacheState { + /// Block state account with account state. + pub accounts: DashMap, + /// Created contracts. + // TODO add bytecode counter for number of bytecodes added/removed. + pub contracts: DashMap, + /// Has EIP-161 state clear enabled (Spurious Dragon hardfork). + pub has_state_clear: bool, +} + +impl From for ConcurrentCacheState { + fn from(other: CacheState) -> Self { + Self { + accounts: other.accounts.into_iter().collect(), + contracts: other.contracts.into_iter().collect(), + has_state_clear: other.has_state_clear, + } + } +} + +impl Default for ConcurrentCacheState { + fn default() -> Self { + Self::new(true) + } +} + +impl ConcurrentCacheState { + /// New default state. + pub fn new(has_state_clear: bool) -> Self { + Self { accounts: DashMap::default(), contracts: DashMap::default(), has_state_clear } + } + + /// Set state clear flag. EIP-161. + pub fn set_state_clear_flag(&mut self, has_state_clear: bool) { + self.has_state_clear = has_state_clear; + } + + /// Insert not existing account. + pub fn insert_not_existing(&self, address: Address) { + self.accounts.insert(address, CacheAccount::new_loaded_not_existing()); + } + + /// Insert Loaded (Or LoadedEmptyEip161 if account is empty) account. + pub fn insert_account(&self, address: Address, info: AccountInfo) { + let account = if !info.is_empty() { + CacheAccount::new_loaded(info, PlainStorage::default()) + } else { + CacheAccount::new_loaded_empty_eip161(PlainStorage::default()) + }; + self.accounts.insert(address, account); + } + + /// Similar to `insert_account` but with storage. + pub fn insert_account_with_storage( + &self, + address: Address, + info: AccountInfo, + storage: PlainStorage, + ) { + let account = if !info.is_empty() { + CacheAccount::new_loaded(info, storage) + } else { + CacheAccount::new_loaded_empty_eip161(storage) + }; + self.accounts.insert(address, account); + } + + /// Apply output of revm execution and create account transitions that are used to build BundleState. + pub fn apply_evm_state(&self, evm_state: EvmState) -> Vec<(Address, TransitionAccount)> { + let mut transitions = Vec::with_capacity(evm_state.len()); + for (address, account) in evm_state { + if let Some(transition) = self.apply_account_state(address, account) { + transitions.push((address, transition)); + } + } + transitions + } + + /// Apply updated account state to the cached account. + /// Returns account transition if applicable. + fn apply_account_state(&self, address: Address, account: Account) -> Option { + // not touched account are never changed. + if !account.is_touched() { + return None; + } + + let mut this_account = + self.accounts.get_mut(&address).expect("All accounts should be present inside cache"); + + // If it is marked as selfdestructed inside revm + // we need to changed state to destroyed. + if account.is_selfdestructed() { + return this_account.selfdestruct(); + } + + let is_created = account.is_created(); + let is_empty = account.is_empty(); + + // transform evm storage to storage with previous value. + let changed_storage = account + .storage + .into_iter() + .filter(|(_, slot)| slot.is_changed()) + .map(|(key, slot)| (key, slot.into())) + .collect(); + + // Note: it can happen that created contract get selfdestructed in same block + // that is why is_created is checked after selfdestructed + // + // Note: Create2 opcode (Petersburg) was after state clear EIP (Spurious Dragon) + // + // Note: It is possibility to create KECCAK_EMPTY contract with some storage + // by just setting storage inside CRATE constructor. Overlap of those contracts + // is not possible because CREATE2 is introduced later. + if is_created { + return Some(this_account.newly_created(account.info, changed_storage)); + } + + // Account is touched, but not selfdestructed or newly created. + // Account can be touched and not changed. + // And when empty account is touched it needs to be removed from database. + // EIP-161 state clear + if is_empty { + if self.has_state_clear { + // touch empty account. + this_account.touch_empty_eip161() + } else { + // if account is empty and state clear is not enabled we should save + // empty account. + this_account.touch_create_pre_eip161(changed_storage) + } + } else { + Some(this_account.change(account.info, changed_storage)) + } + } +} + +// Some code above and documentation is adapted from the revm crate, and is +// reproduced here under the terms of the MIT license. +// +// MIT License +// +// Copyright (c) 2021-2024 draganrakita +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..c0aa2ad --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,43 @@ +mod builder; + +pub use builder::ConcurrentStateBuilder; + +mod cache_state; +pub use cache_state::ConcurrentCacheState; + +mod sync_state; +pub use sync_state::{ConcurrentState, ConcurrentStateInfo}; + +use crate::{EvmNeedsBlock, Trevm}; +use revm::{ + db::{states::bundle_state::BundleRetention, BundleState}, + DatabaseRef, +}; + +impl Trevm<'_, Ext, ConcurrentState, TrevmState> { + /// Set the [EIP-161] state clear flag, activated in the Spurious Dragon + /// hardfork. + /// + /// This function changes the behavior of the inner [`ConcurrentState`]. + pub fn set_state_clear_flag(&mut self, flag: bool) { + self.inner.db_mut().set_state_clear_flag(flag) + } +} + +impl EvmNeedsBlock<'_, Ext, ConcurrentState> { + /// Finish execution and return the outputs. + /// + /// If the State has not been built with + /// [revm::StateBuilder::with_bundle_update] then the returned + /// [`BundleState`] will be meaningless. + /// + /// See [`ConcurrentState::merge_transitions`] and + /// [`ConcurrentState::take_bundle`]. + pub fn finish(self) -> BundleState { + let Self { inner: mut evm, .. } = self; + evm.db_mut().merge_transitions(BundleRetention::Reverts); + let bundle = evm.db_mut().take_bundle(); + + bundle + } +} diff --git a/src/db/sync_state.rs b/src/db/sync_state.rs new file mode 100644 index 0000000..5b25bd7 --- /dev/null +++ b/src/db/sync_state.rs @@ -0,0 +1,378 @@ +use crate::db::ConcurrentCacheState; +use alloc::{collections::BTreeMap, vec::Vec}; +use alloy_primitives::{Address, B256, U256}; +use dashmap::mapref::one::RefMut; +use revm::{ + db::{ + states::{bundle_state::BundleRetention, plain_account::PlainStorage, CacheAccount}, + BundleState, State, + }, + primitives::{Account, AccountInfo, Bytecode}, + Database, DatabaseCommit, DatabaseRef, TransitionAccount, TransitionState, +}; +use std::{collections::hash_map, sync::RwLock}; + +/// State of the blockchain. +/// +/// A version of [`revm::db::State`] that can be shared between threads. +#[derive(Debug)] +pub struct ConcurrentState { + database: Db, + /// Non-DB state cache and transition information. + pub info: ConcurrentStateInfo, +} + +impl From> for ConcurrentState +where + Db: DatabaseRef, +{ + fn from(value: State) -> Self { + Self { + database: value.database, + info: ConcurrentStateInfo { + cache: value.cache.into(), + transition_state: value.transition_state, + bundle_state: value.bundle_state, + use_preloaded_bundle: value.use_preloaded_bundle, + block_hashes: value.block_hashes.into(), + }, + } + } +} + +/// Non-DB contents of [`ConcurrentState`] +#[derive(Debug, Default)] +pub struct ConcurrentStateInfo { + /// Cached state contains both changed from evm execution and cached/loaded + /// account/storages from database. This allows us to have only one layer + /// of cache where we can fetch data. Additionally we can introduce some + /// preloading of data from database. + pub cache: ConcurrentCacheState, + /// Block state, it aggregates transactions transitions into one state. + /// + /// Build reverts and state that gets applied to the state. + pub transition_state: Option, + /// After block is finishes we merge those changes inside bundle. + /// Bundle is used to update database and create changesets. + /// Bundle state can be set on initialization if we want to use preloaded + /// bundle. + pub bundle_state: BundleState, + /// Addition layer that is going to be used to fetched values before + /// fetching values from database. + /// + /// Bundle is the main output of the state execution and this allows + /// setting previous bundle and using its values for execution. + pub use_preloaded_bundle: bool, + /// If EVM asks for block hash we will first check if they are found here. + /// and then ask the database. + /// + /// This map can be used to give different values for block hashes if in + /// case the fork block is different or some blocks are not saved inside + /// database. + pub block_hashes: RwLock>, +} + +impl ConcurrentState { + /// Create a new [`ConcurrentState`] with the given database and cache + /// state. + pub const fn new(database: Db, info: ConcurrentStateInfo) -> Self { + Self { database, info } + } + + /// Deconstruct the [`ConcurrentState`] into its parts. + pub fn into_parts(self) -> (Db, ConcurrentStateInfo) { + (self.database, self.info) + } + + /// Returns the size hint for the inner bundle state. + /// See [BundleState::size_hint] for more info. + pub fn bundle_size_hint(&self) -> usize { + self.info.bundle_state.size_hint() + } + + /// Iterate over received balances and increment all account balances. + /// If account is not found inside cache state it will be loaded from database. + /// + /// Update will create transitions for all accounts that are updated. + /// + /// Like [`CacheAccount::increment_balance`], this assumes that incremented balances are not + /// zero, and will not overflow once incremented. If using this to implement withdrawals, zero + /// balances must be filtered out before calling this function. + pub fn increment_balances( + &mut self, + balances: impl IntoIterator, + ) -> Result<(), Db::Error> { + // make transition and update cache state + let mut transitions = Vec::new(); + for (address, balance) in balances { + if balance == 0 { + continue; + } + let mut original_account = self.load_cache_account_mut(address)?; + transitions.push(( + address, + original_account.increment_balance(balance).expect("Balance is not zero"), + )) + } + // append transition + if let Some(s) = self.info.transition_state.as_mut() { + s.add_transitions(transitions) + } + Ok(()) + } + + /// Drain balances from given account and return those values. + /// + /// It is used for DAO hardfork state change to move values from given accounts. + pub fn drain_balances( + &mut self, + addresses: impl IntoIterator, + ) -> Result, Db::Error> { + // make transition and update cache state + let mut transitions = Vec::new(); + let mut balances = Vec::new(); + for address in addresses { + let mut original_account = self.load_cache_account_mut(address)?; + let (balance, transition) = original_account.drain_balance(); + balances.push(balance); + transitions.push((address, transition)) + } + // append transition + if let Some(s) = self.info.transition_state.as_mut() { + s.add_transitions(transitions) + } + Ok(balances) + } + + /// State clear EIP-161 is enabled in Spurious Dragon hardfork. + pub fn set_state_clear_flag(&mut self, has_state_clear: bool) { + self.info.cache.set_state_clear_flag(has_state_clear); + } + + /// Insert not existing account into cache state. + pub fn insert_not_existing(&mut self, address: Address) { + self.info.cache.insert_not_existing(address) + } + + /// Insert account into cache state. + pub fn insert_account(&mut self, address: Address, info: AccountInfo) { + self.info.cache.insert_account(address, info) + } + + /// Insert account with storage into cache state. + pub fn insert_account_with_storage( + &mut self, + address: Address, + info: AccountInfo, + storage: PlainStorage, + ) { + self.info.cache.insert_account_with_storage(address, info, storage) + } + + /// Apply evm transitions to transition state. + pub fn apply_transition(&mut self, transitions: Vec<(Address, TransitionAccount)>) { + // add transition to transition state. + if let Some(s) = self.info.transition_state.as_mut() { + s.add_transitions(transitions) + } + } + + /// Take all transitions and merge them inside bundle state. + /// This action will create final post state and all reverts so that + /// we at any time revert state of bundle to the state before transition + /// is applied. + pub fn merge_transitions(&mut self, retention: BundleRetention) { + if let Some(transition_state) = self.info.transition_state.take() { + self.info + .bundle_state + .apply_transitions_and_create_reverts(transition_state, retention); + } + } + + /// Get a mutable reference to the [`CacheAccount`] for the given address. + /// If the account is not found in the cache, it will be loaded from the + /// database and inserted into the cache. + /// + /// This function locks that account in the cache while the reference is + /// held. For that timeframe, it will block other tasks attempting to + /// access that account. As a result, this function should be used + /// sparingly. + pub fn load_cache_account_mut( + &self, + address: Address, + ) -> Result, Db::Error> { + match self.info.cache.accounts.entry(address) { + dashmap::Entry::Vacant(entry) => { + if self.info.use_preloaded_bundle { + // load account from bundle state + if let Some(account) = + self.info.bundle_state.account(&address).cloned().map(Into::into) + { + return Ok(entry.insert(account)); + } + } + // if not found in bundle, load it from database + let info = self.database.basic_ref(address)?; + let account = match info { + None => CacheAccount::new_loaded_not_existing(), + Some(acc) if acc.is_empty() => { + CacheAccount::new_loaded_empty_eip161(PlainStorage::default()) + } + Some(acc) => CacheAccount::new_loaded(acc, PlainStorage::default()), + }; + Ok(entry.insert(account)) + } + dashmap::Entry::Occupied(entry) => Ok(entry.into_ref()), + } + } + + // TODO make cache aware of transitions dropping by having global transition counter. + /// Takes the [`BundleState`] changeset from the [`ConcurrentState`], + /// replacing it + /// with an empty one. + /// + /// This will not apply any pending [`TransitionState`]. It is recommended + /// to call [`ConcurrentState::merge_transitions`] before taking the bundle. + /// + /// If the `State` has been built with the + /// [`revm::StateBuilder::with_bundle_prestate`] option, the pre-state will be + /// taken along with any changes made by + /// [`ConcurrentState::merge_transitions`]. + pub fn take_bundle(&mut self) -> BundleState { + core::mem::take(&mut self.info.bundle_state) + } +} + +impl DatabaseRef for ConcurrentState { + type Error = Db::Error; + + fn basic_ref(&self, address: Address) -> Result, Self::Error> { + self.load_cache_account_mut(address).map(|a| a.account_info()) + } + + fn code_by_hash_ref(&self, code_hash: B256) -> Result { + let res = match self.info.cache.contracts.entry(code_hash) { + dashmap::Entry::Occupied(entry) => Ok(entry.get().clone()), + dashmap::Entry::Vacant(entry) => { + if self.info.use_preloaded_bundle { + if let Some(code) = self.info.bundle_state.contracts.get(&code_hash) { + entry.insert(code.clone()); + return Ok(code.clone()); + } + } + // if not found in bundle ask database + let code = self.database.code_by_hash_ref(code_hash)?; + entry.insert(code.clone()); + Ok(code) + } + }; + res + } + + fn storage_ref(&self, address: Address, index: U256) -> Result { + // Account is guaranteed to be loaded. + // Note that storage from bundle is already loaded with account. + if let Some(mut account) = self.info.cache.accounts.get_mut(&address) { + // account will always be some, but if it is not, U256::ZERO will be returned. + let is_storage_known = account.status.is_storage_known(); + Ok(account + .account + .as_mut() + .map(|account| match account.storage.entry(index) { + hash_map::Entry::Occupied(entry) => Ok(*entry.get()), + hash_map::Entry::Vacant(entry) => { + // if account was destroyed or account is newly built + // we return zero and don't ask database. + let value = if is_storage_known { + U256::ZERO + } else { + self.database.storage_ref(address, index)? + }; + entry.insert(value); + Ok(value) + } + }) + .transpose()? + .unwrap_or_default()) + } else { + unreachable!("For accessing any storage account is guaranteed to be loaded beforehand") + } + } + + fn block_hash_ref(&self, number: u64) -> Result { + { + let hashes = self.info.block_hashes.read().unwrap(); + if let Some(hash) = hashes.get(&number) { + return Ok(*hash); + } + } + + let hash = self.database.block_hash_ref(number)?; + + let mut hashes = self.info.block_hashes.write().unwrap(); + + hashes.insert(number, hash); + + // prune all hashes that are older then BLOCK_HASH_HISTORY + let last_block = number.saturating_sub(revm::primitives::BLOCK_HASH_HISTORY); + + // lock the hashes, split at the key, and retain the newer hashes + let mut hashes = self.info.block_hashes.write().unwrap(); + let to_retain = hashes.split_off(&last_block); + *hashes = to_retain; + + Ok(hash) + } +} + +impl DatabaseCommit for ConcurrentState { + fn commit(&mut self, evm_state: revm::primitives::HashMap) { + let transitions = self.info.cache.apply_evm_state(evm_state); + self.apply_transition(transitions); + } +} + +impl Database for ConcurrentState { + type Error = ::Error; + + fn basic(&mut self, address: Address) -> Result, Self::Error> { + self.basic_ref(address) + } + + fn code_by_hash(&mut self, code_hash: B256) -> Result { + self.code_by_hash_ref(code_hash) + } + + fn storage(&mut self, address: Address, index: U256) -> Result { + self.storage_ref(address, index) + } + + fn block_hash(&mut self, number: u64) -> Result { + self.block_hash_ref(number) + } +} + +// Some code above and documentation is adapted from the revm crate, and is +// reproduced here under the terms of the MIT license. +// +// MIT License +// +// Copyright (c) 2021-2024 draganrakita +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. diff --git a/src/evm.rs b/src/evm.rs index f466378..3fb4127 100644 --- a/src/evm.rs +++ b/src/evm.rs @@ -20,8 +20,8 @@ use revm::{ /// /// See the [crate-level documentation](crate) for more information. pub struct Trevm<'a, Ext, Db: Database + DatabaseCommit, TrevmState> { - inner: Box>, - state: TrevmState, + pub(crate) inner: Box>, + pub(crate) state: TrevmState, } impl fmt::Debug for Trevm<'_, Ext, Db, TrevmState> { @@ -440,7 +440,7 @@ impl + DatabaseCommit, TrevmState> } } -// --- ALL STATES, WITH STATE +// --- ALL STATES, WITH State impl Trevm<'_, Ext, State, TrevmState> { /// Set the [EIP-161] state clear flag, activated in the Spurious Dragon @@ -883,9 +883,9 @@ impl<'a, Ext, Db: Database + DatabaseCommit, TrevmState: HasBlock> Trevm<'a, Ext impl EvmNeedsBlock<'_, Ext, State> { /// Finish execution and return the outputs. /// - /// ## Panics - /// - /// If the State has not been built with StateBuilder::with_bundle_update. + /// If the State has not been built with + /// [revm::StateBuilder::with_bundle_update] then the returned + /// [`BundleState`] will be meaningless. /// /// See [`State::merge_transitions`] and [`State::take_bundle`]. pub fn finish(self) -> BundleState { diff --git a/src/lib.rs b/src/lib.rs index 03c6ecc..689a7c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -366,6 +366,10 @@ extern crate alloc; mod connect; pub use connect::{DbConnect, EvmFactory}; +/// Contains database implementations for concurrent EVM operation. +#[cfg(feature = "concurrent-db")] +pub mod db; + mod driver; pub use driver::{ BlockDriver, BundleDriver, ChainDriver, DriveBlockResult, DriveBundleResult, DriveChainResult,