diff --git a/crates/context/interface/src/journaled_state.rs b/crates/context/interface/src/journaled_state.rs index 8559699677..b4e47a0ac7 100644 --- a/crates/context/interface/src/journaled_state.rs +++ b/crates/context/interface/src/journaled_state.rs @@ -6,7 +6,7 @@ pub mod entry; use crate::{ context::{SStoreResult, SelfDestructResult}, host::LoadError, - journaled_state::{account::JournaledAccount, entry::JournalEntryTr}, + journaled_state::account::JournaledAccountTr, }; use core::ops::{Deref, DerefMut}; use database_interface::Database; @@ -15,15 +15,18 @@ use primitives::{ }; use state::{Account, AccountInfo, Bytecode}; use std::{borrow::Cow, vec::Vec}; - /// Trait that contains database and journal of all changes that were made to the state. pub trait JournalTr { /// Database type that is used in the journal. type Database: Database; /// State type that is returned by the journal after finalization. type State; - /// Journal Entry type that is used in the journal. - type JournalEntry: JournalEntryTr; + /// Journaled account type that is used in the journal. + type JournaledAccount<'a>: JournaledAccountTr< + DatabaseError = ::Error, + > + where + Self: 'a; /// Creates new Journaled state. /// @@ -186,10 +189,7 @@ pub trait JournalTr { fn load_account_mut( &mut self, address: Address, - ) -> Result< - StateLoad>, - ::Error, - > { + ) -> Result>, ::Error> { self.load_account_mut_optional_code(address, false) } @@ -198,10 +198,7 @@ pub trait JournalTr { fn load_account_with_code_mut( &mut self, address: Address, - ) -> Result< - StateLoad>, - ::Error, - > { + ) -> Result>, ::Error> { self.load_account_mut_optional_code(address, true) } @@ -210,10 +207,7 @@ pub trait JournalTr { &mut self, address: Address, load_code: bool, - ) -> Result< - StateLoad>, - ::Error, - >; + ) -> Result>, ::Error>; /// Sets bytecode with hash. Assume that account is warm. fn set_code_with_hash(&mut self, address: Address, code: Bytecode, hash: B256); @@ -457,6 +451,7 @@ pub struct AccountInfoLoad<'a> { impl<'a> AccountInfoLoad<'a> { /// Creates new [`AccountInfoLoad`] with the given account info, cold load status and empty status. + #[inline] pub fn new(account: &'a AccountInfo, is_cold: bool, is_empty: bool) -> Self { Self { account: Cow::Borrowed(account), @@ -468,6 +463,7 @@ impl<'a> AccountInfoLoad<'a> { /// Maps the account info of the [`AccountInfoLoad`] to a new [`StateLoad`]. /// /// Useful for transforming the account info of the [`AccountInfoLoad`] and preserving the cold load status. + #[inline] pub fn into_state_load(self, f: F) -> StateLoad where F: FnOnce(Cow<'a, AccountInfo>) -> O, diff --git a/crates/context/interface/src/journaled_state/account.rs b/crates/context/interface/src/journaled_state/account.rs index 2e219d4938..da37c19def 100644 --- a/crates/context/interface/src/journaled_state/account.rs +++ b/crates/context/interface/src/journaled_state/account.rs @@ -3,73 +3,192 @@ //! //! Useful to encapsulate account and journal entries together. So when account gets changed, we can add a journal entry for it. +use crate::{ + context::{SStoreResult, StateLoad}, + journaled_state::JournalLoadError, +}; + use super::entry::JournalEntryTr; +use auto_impl::auto_impl; use core::ops::Deref; -use primitives::{Address, B256, KECCAK_EMPTY, U256}; -use state::{Account, Bytecode}; +use database_interface::Database; +use primitives::{ + hash_map::Entry, Address, HashMap, HashSet, StorageKey, StorageValue, B256, KECCAK_EMPTY, U256, +}; +use state::{Account, Bytecode, EvmStorageSlot}; use std::vec::Vec; +/// Trait that contains database and journal of all changes that were made to the account. +#[auto_impl(&mut, Box)] +pub trait JournaledAccountTr: Deref { + /// Database error type. + type DatabaseError; + + /// Creates a new journaled account. + fn sload( + &mut self, + key: StorageKey, + skip_cold_load: bool, + ) -> Result, JournalLoadError>; + + /// Warm loads storage slot and stores the new value + fn sstore( + &mut self, + key: StorageKey, + new: StorageValue, + skip_cold_load: bool, + ) -> Result, JournalLoadError>; + + /// Loads the code of the account. and returns it as reference. + fn load_code(&mut self) -> Result<&Bytecode, JournalLoadError>; + + /// Returns the balance of the account. + fn balance(&self) -> &U256; + + /// Returns the nonce of the account. + fn nonce(&self) -> u64; + + /// Returns the code hash of the account. + fn code_hash(&self) -> &B256; + + /// Returns the code of the account. + fn code(&self) -> Option<&Bytecode>; + + /// Touches the account. + fn touch(&mut self); + + /// Marks the account as cold without making a journal entry. + /// + /// Changing account without journal entry can be a footgun as reverting of the state change + /// would not happen without entry. It is the reason why this function has an `unsafe` prefix. + /// + /// If account is in access list, it would still be marked as warm if account get accessed again. + fn unsafe_mark_cold(&mut self); + + /// Sets the balance of the account. + /// + /// If balance is the same, we don't add a journal entry. + /// + /// Touches the account in all cases. + fn set_balance(&mut self, balance: U256); + + /// Increments the balance of the account. + /// + /// Touches the account in all cases. + fn incr_balance(&mut self, balance: U256) -> bool; + + /// Decrements the balance of the account. + /// + /// Touches the account in all cases. + fn decr_balance(&mut self, balance: U256) -> bool; + + /// Bumps the nonce of the account. + /// + /// Touches the account in all cases. + /// + /// Returns true if nonce was bumped, false if nonce is at the max value. + fn bump_nonce(&mut self) -> bool; + + /// Set the nonce of the account and create a journal entry. + /// + /// Touches the account in all cases. + fn set_nonce(&mut self, nonce: u64); + + /// Set the nonce of the account without creating a journal entry. + /// + /// Changing account without journal entry can be a footgun as reverting of the state change + /// would not happen without entry. It is the reason why this function has an `unsafe` prefix. + fn unsafe_set_nonce(&mut self, nonce: u64); + + /// Sets the code of the account. + /// + /// Touches the account in all cases. + fn set_code(&mut self, code_hash: B256, code: Bytecode); + + /// Sets the code of the account. Calculates hash of the code. + /// + /// Touches the account in all cases. + fn set_code_and_hash_slow(&mut self, code: Bytecode); + + /// Delegates the account to another address (EIP-7702). + /// + /// This touches the account, sets the code to the delegation designation, + /// and bumps the nonce. + fn delegate(&mut self, address: Address); +} + /// Journaled account contains both mutable account and journal entries. /// /// Useful to encapsulate account and journal entries together. So when account gets changed, we can add a journal entry for it. #[derive(Debug, PartialEq, Eq)] -pub struct JournaledAccount<'a, ENTRY: JournalEntryTr> { +pub struct JournaledAccount<'a, ENTRY: JournalEntryTr, DB> { /// Address of the account. address: Address, /// Mutable account. account: &'a mut Account, /// Journal entries. journal_entries: &'a mut Vec, + /// Access list. + access_list: &'a HashMap>, + /// Transaction ID. + transaction_id: usize, + /// Database used to load storage. + db: &'a mut DB, } -impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { - /// Consumes the journaled account and returns the mutable account. - #[inline] - pub fn into_account_ref(self) -> &'a Account { - self.account +impl<'a, ENTRY: JournalEntryTr, DB: Database> JournaledAccountTr + for JournaledAccount<'a, ENTRY, DB> +{ + type DatabaseError = ::Error; + + fn sload( + &mut self, + key: StorageKey, + skip_cold_load: bool, + ) -> Result, JournalLoadError> { + self.sload(key, skip_cold_load) } - /// Creates a new journaled account. - #[inline] - pub fn new( - address: Address, - account: &'a mut Account, - journal_entries: &'a mut Vec, - ) -> Self { - Self { - address, - account, - journal_entries, - } + fn sstore( + &mut self, + key: StorageKey, + new: StorageValue, + skip_cold_load: bool, + ) -> Result, JournalLoadError> { + self.sstore(key, new, skip_cold_load) + } + + fn load_code(&mut self) -> Result<&Bytecode, JournalLoadError> { + self.load_code() } /// Returns the balance of the account. #[inline] - pub fn balance(&self) -> &U256 { + fn balance(&self) -> &U256 { &self.account.info.balance } /// Returns the nonce of the account. #[inline] - pub fn nonce(&self) -> u64 { + fn nonce(&self) -> u64 { self.account.info.nonce } /// Returns the code hash of the account. #[inline] - pub fn code_hash(&self) -> &B256 { + fn code_hash(&self) -> &B256 { &self.account.info.code_hash } /// Returns the code of the account. #[inline] - pub fn code(&self) -> Option<&Bytecode> { + fn code(&self) -> Option<&Bytecode> { self.account.info.code.as_ref() } /// Touches the account. #[inline] - pub fn touch(&mut self) { + fn touch(&mut self) { if !self.account.status.is_touched() { self.account.mark_touch(); self.journal_entries @@ -84,7 +203,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// If account is in access list, it would still be marked as warm if account get accessed again. #[inline] - pub fn unsafe_mark_cold(&mut self) { + fn unsafe_mark_cold(&mut self) { self.account.mark_cold(); } @@ -94,7 +213,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Touches the account in all cases. #[inline] - pub fn set_balance(&mut self, balance: U256) { + fn set_balance(&mut self, balance: U256) { self.touch(); if self.account.info.balance != balance { self.journal_entries.push(ENTRY::balance_changed( @@ -109,7 +228,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Touches the account in all cases. #[inline] - pub fn incr_balance(&mut self, balance: U256) -> bool { + fn incr_balance(&mut self, balance: U256) -> bool { self.touch(); let Some(balance) = self.account.info.balance.checked_add(balance) else { return false; @@ -122,7 +241,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Touches the account in all cases. #[inline] - pub fn decr_balance(&mut self, balance: U256) -> bool { + fn decr_balance(&mut self, balance: U256) -> bool { self.touch(); let Some(balance) = self.account.info.balance.checked_sub(balance) else { return false; @@ -137,7 +256,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Returns true if nonce was bumped, false if nonce is at the max value. #[inline] - pub fn bump_nonce(&mut self) -> bool { + fn bump_nonce(&mut self) -> bool { self.touch(); let Some(nonce) = self.account.info.nonce.checked_add(1) else { return false; @@ -151,7 +270,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Touches the account in all cases. #[inline] - pub fn set_nonce(&mut self, nonce: u64) { + fn set_nonce(&mut self, nonce: u64) { self.touch(); let previous_nonce = self.account.info.nonce; self.account.info.set_nonce(nonce); @@ -164,7 +283,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// Changing account without journal entry can be a footgun as reverting of the state change /// would not happen without entry. It is the reason why this function has an `unsafe` prefix. #[inline] - pub fn unsafe_set_nonce(&mut self, nonce: u64) { + fn unsafe_set_nonce(&mut self, nonce: u64) { self.account.info.set_nonce(nonce); } @@ -172,7 +291,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Touches the account in all cases. #[inline] - pub fn set_code(&mut self, code_hash: B256, code: Bytecode) { + fn set_code(&mut self, code_hash: B256, code: Bytecode) { self.touch(); self.account.info.set_code_hash(code_hash); self.account.info.set_code(code); @@ -183,7 +302,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// /// Touches the account in all cases. #[inline] - pub fn set_code_and_hash_slow(&mut self, code: Bytecode) { + fn set_code_and_hash_slow(&mut self, code: Bytecode) { let code_hash = code.hash_slow(); self.set_code(code_hash, code); } @@ -193,7 +312,7 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { /// This touches the account, sets the code to the delegation designation, /// and bumps the nonce. #[inline] - pub fn delegate(&mut self, address: Address) { + fn delegate(&mut self, address: Address) { let (bytecode, hash) = if address.is_zero() { (Bytecode::default(), KECCAK_EMPTY) } else { @@ -207,7 +326,141 @@ impl<'a, ENTRY: JournalEntryTr> JournaledAccount<'a, ENTRY> { } } -impl<'a, ENTRY: JournalEntryTr> Deref for JournaledAccount<'a, ENTRY> { +impl<'a, ENTRY: JournalEntryTr, DB: Database> JournaledAccount<'a, ENTRY, DB> { + /// Creates a new journaled account. + #[inline(never)] + pub fn sload( + &mut self, + key: StorageKey, + skip_cold_load: bool, + ) -> Result, JournalLoadError<::Error>> { + let is_newly_created = self.account.is_created(); + let (slot, is_cold) = match self.account.storage.entry(key) { + Entry::Occupied(occ) => { + let slot = occ.into_mut(); + // skip load if account is cold. + let is_cold = slot.is_cold_transaction_id(self.transaction_id); + if is_cold && skip_cold_load { + return Err(JournalLoadError::ColdLoadSkipped); + } + slot.mark_warm_with_transaction_id(self.transaction_id); + (slot, is_cold) + } + Entry::Vacant(vac) => { + // is storage cold + let is_cold = self + .access_list + .get(&self.address) + .and_then(|v| v.get(&key)) + .is_none(); + + if is_cold && skip_cold_load { + return Err(JournalLoadError::ColdLoadSkipped); + } + // if storage was cleared, we don't need to ping db. + let value = if is_newly_created { + StorageValue::ZERO + } else { + self.db.storage(self.address, key)? + }; + + let slot = vac.insert(EvmStorageSlot::new(value, self.transaction_id)); + (slot, is_cold) + } + }; + + if is_cold { + // add it to journal as cold loaded. + self.journal_entries + .push(ENTRY::storage_warmed(self.address, key)); + } + + Ok(StateLoad::new(slot, is_cold)) + } + + /// Warm loads storage slot and stores the new value + #[inline] + pub fn sstore( + &mut self, + key: StorageKey, + new: StorageValue, + skip_cold_load: bool, + ) -> Result, JournalLoadError<::Error>> { + // touch the account so changes are tracked. + self.touch(); + + // assume that acc exists and load the slot. + let slot = self.sload(key, skip_cold_load)?; + + let ret = Ok(StateLoad::new( + SStoreResult { + original_value: slot.original_value(), + present_value: slot.present_value(), + new_value: new, + }, + slot.is_cold, + )); + + // when new value is different from present, we need to add a journal entry and make a change. + if slot.present_value != new { + let previous_value = slot.present_value; + // insert value into present state. + slot.data.present_value = new; + + // add journal entry. + self.journal_entries + .push(ENTRY::storage_changed(self.address, key, previous_value)); + } + + ret + } + + /// Loads the code of the account. and returns it as reference. + #[inline] + pub fn load_code(&mut self) -> Result<&Bytecode, JournalLoadError<::Error>> { + if self.account.info.code.is_none() { + let hash = *self.code_hash(); + let code = if hash == KECCAK_EMPTY { + Bytecode::default() + } else { + self.db.code_by_hash(hash)? + }; + self.account.info.code = Some(code); + } + + Ok(self.account.info.code.as_ref().unwrap()) + } +} + +impl<'a, ENTRY: JournalEntryTr, DB> JournaledAccount<'a, ENTRY, DB> { + /// Consumes the journaled account and returns the mutable account. + #[inline] + pub fn into_account_ref(self) -> &'a Account { + self.account + } + + /// Creates a new journaled account. + #[inline] + pub fn new( + address: Address, + account: &'a mut Account, + journal_entries: &'a mut Vec, + db: &'a mut DB, + access_list: &'a HashMap>, + transaction_id: usize, + ) -> Self { + Self { + address, + account, + journal_entries, + db, + access_list, + transaction_id, + } + } +} + +impl<'a, ENTRY: JournalEntryTr, DB> Deref for JournaledAccount<'a, ENTRY, DB> { type Target = Account; fn deref(&self) -> &Self::Target { diff --git a/crates/context/src/context.rs b/crates/context/src/context.rs index f03ac49d19..5bf1832fa3 100644 --- a/crates/context/src/context.rs +++ b/crates/context/src/context.rs @@ -8,7 +8,9 @@ use context_interface::{ }; use database_interface::{Database, DatabaseRef, EmptyDB, WrapDatabaseRef}; use derive_where::derive_where; -use primitives::{hardfork::SpecId, Address, Log, StorageKey, StorageValue, B256, U256}; +use primitives::{ + hardfork::SpecId, hints_util::cold_path, Address, Log, StorageKey, StorageValue, B256, U256, +}; /// EVM context contains data that EVM needs for execution. #[derive_where(Clone, Debug; BLOCK, CFG, CHAIN, TX, DB, JOURNAL, ::Error, LOCAL)] @@ -499,6 +501,7 @@ impl< self.db_mut() .block_hash(requested_number) .map_err(|e| { + cold_path(); *self.error() = Err(e.into()); }) .ok() @@ -532,6 +535,7 @@ impl< self.journal_mut() .selfdestruct(address, target, skip_cold_load) .map_err(|e| { + cold_path(); let (ret, err) = e.into_parts(); if let Some(err) = err { *self.error() = Err(err.into()); @@ -551,6 +555,7 @@ impl< self.journal_mut() .sstore_skip_cold_load(address, key, value, skip_cold_load) .map_err(|e| { + cold_path(); let (ret, err) = e.into_parts(); if let Some(err) = err { *self.error() = Err(err.into()); @@ -569,6 +574,7 @@ impl< self.journal_mut() .sload_skip_cold_load(address, key, skip_cold_load) .map_err(|e| { + cold_path(); let (ret, err) = e.into_parts(); if let Some(err) = err { *self.error() = Err(err.into()); @@ -584,14 +590,17 @@ impl< load_code: bool, skip_cold_load: bool, ) -> Result, LoadError> { - let error = &mut self.error; - let journal = &mut self.journaled_state; - match journal.load_account_info_skip_cold_load(address, load_code, skip_cold_load) { + match self.journaled_state.load_account_info_skip_cold_load( + address, + load_code, + skip_cold_load, + ) { Ok(a) => Ok(a), Err(e) => { + cold_path(); let (ret, err) = e.into_parts(); if let Some(err) = err { - *error = Err(err.into()); + self.error = Err(err.into()); } Err(ret) } diff --git a/crates/context/src/journal.rs b/crates/context/src/journal.rs index dd1a85b89c..e502098817 100644 --- a/crates/context/src/journal.rs +++ b/crates/context/src/journal.rs @@ -92,7 +92,11 @@ impl Journal { impl JournalTr for Journal { type Database = DB; type State = EvmState; - type JournalEntry = ENTRY; + type JournaledAccount<'a> + = JournaledAccount<'a, ENTRY, DB> + where + ENTRY: 'a, + DB: 'a; fn new(database: DB) -> Journal { Self { @@ -115,7 +119,7 @@ impl JournalTr for Journal { key: StorageKey, ) -> Result, ::Error> { self.inner - .sload(&mut self.database, address, key, false) + .sload_unsafe(&mut self.database, address, key, false) .map_err(JournalLoadError::unwrap_db_error) } @@ -126,7 +130,7 @@ impl JournalTr for Journal { value: StorageValue, ) -> Result, ::Error> { self.inner - .sstore(&mut self.database, address, key, value, false) + .sstore_unsafe(&mut self.database, address, key, value, false) .map_err(JournalLoadError::unwrap_db_error) } @@ -257,10 +261,7 @@ impl JournalTr for Journal { &mut self, address: Address, load_code: bool, - ) -> Result< - StateLoad>, - ::Error, - > { + ) -> Result>, ::Error> { self.inner .load_account_mut_optional_code(&mut self.database, address, load_code, false) .map_err(JournalLoadError::unwrap_db_error) @@ -341,7 +342,7 @@ impl JournalTr for Journal { ) -> Result, JournalLoadError<::Error>> { self.inner - .sload(&mut self.database, address, key, skip_cold_load) + .sload_unsafe(&mut self.database, address, key, skip_cold_load) } #[inline] @@ -354,7 +355,7 @@ impl JournalTr for Journal { ) -> Result, JournalLoadError<::Error>> { self.inner - .sstore(&mut self.database, address, key, value, skip_cold_load) + .sstore_unsafe(&mut self.database, address, key, value, skip_cold_load) } #[inline] diff --git a/crates/context/src/journal/inner.rs b/crates/context/src/journal/inner.rs index a624a3e378..738a64ccd3 100644 --- a/crates/context/src/journal/inner.rs +++ b/crates/context/src/journal/inner.rs @@ -4,7 +4,7 @@ use bytecode::Bytecode; use context_interface::{ context::{SStoreResult, SelfDestructResult, StateLoad}, journaled_state::{ - account::JournaledAccount, + account::{JournaledAccount, JournaledAccountTr}, entry::{JournalEntryTr, SelfdestructionRevertStatus}, AccountLoad, JournalCheckpoint, JournalLoadError, TransferError, }, @@ -14,9 +14,10 @@ use database_interface::Database; use primitives::{ hardfork::SpecId::{self, *}, hash_map::Entry, + hints_util::unlikely, Address, HashMap, Log, StorageKey, StorageValue, B256, KECCAK_EMPTY, U256, }; -use state::{Account, EvmState, EvmStorageSlot, TransientStorage}; +use state::{Account, EvmState, TransientStorage}; use std::vec::Vec; /// Inner journal state that contains journal and state changes. /// @@ -565,11 +566,14 @@ impl JournalInner { /// Loads account into memory. return if it is cold or warm accessed #[inline] - pub fn load_account( - &mut self, - db: &mut DB, + pub fn load_account<'a, 'b, DB: Database>( + &'a mut self, + db: &'b mut DB, address: Address, - ) -> Result, DB::Error> { + ) -> Result, DB::Error> + where + 'b: 'a, + { self.load_account_optional(db, address, false, false) .map_err(JournalLoadError::unwrap_db_error) } @@ -621,63 +625,118 @@ impl JournalInner { /// In case of EIP-7702 delegated account will not be loaded, /// [`Self::load_account_delegated`] should be used instead. #[inline] - pub fn load_code( - &mut self, - db: &mut DB, + pub fn load_code<'a, 'b, DB: Database>( + &'a mut self, + db: &'a mut DB, address: Address, - ) -> Result, DB::Error> { + ) -> Result, DB::Error> + where + 'b: 'a, + { self.load_account_optional(db, address, true, false) .map_err(JournalLoadError::unwrap_db_error) } /// Loads account into memory. If account is already loaded it will be marked as warm. #[inline] - pub fn load_account_optional( - &mut self, - db: &mut DB, + pub fn load_account_optional<'a, 'b, DB: Database>( + &'a mut self, + db: &'a mut DB, address: Address, load_code: bool, skip_cold_load: bool, - ) -> Result, JournalLoadError> { - let load = self.load_account_mut_optional_code(db, address, load_code, skip_cold_load)?; + ) -> Result, JournalLoadError> + where + 'b: 'a, + { + let mut load = self.load_account_mut_optional(db, address, skip_cold_load)?; + if load_code { + load.data.load_code()?; + } Ok(load.map(|i| i.into_account_ref())) } /// Loads account into memory. If account is already loaded it will be marked as warm. #[inline] - pub fn load_account_mut( - &mut self, - db: &mut DB, + pub fn load_account_mut<'a, 'b, DB: Database>( + &'a mut self, + db: &'b mut DB, address: Address, - ) -> Result>, DB::Error> { - self.load_account_mut_optional_code(db, address, false, false) + ) -> Result>, DB::Error> + where + 'b: 'a, + { + self.load_account_mut_optional(db, address, false) .map_err(JournalLoadError::unwrap_db_error) } /// Loads account. If account is already loaded it will be marked as warm. - #[inline(never)] - pub fn load_account_mut_optional_code( - &mut self, - db: &mut DB, + #[inline] + pub fn load_account_mut_optional_code<'a, 'b, DB: Database>( + &'a mut self, + db: &'b mut DB, address: Address, load_code: bool, skip_cold_load: bool, - ) -> Result>, JournalLoadError> { - let load = match self.state.entry(address) { + ) -> Result>, JournalLoadError> + where + 'b: 'a, + { + let mut load = self.load_account_mut_optional(db, address, skip_cold_load)?; + if load_code { + load.data.load_code()?; + } + Ok(load) + } + + /// Gets the account mut reference. + /// + /// # Load Unsafe + /// + /// Use this function only if you know what you are doing. It will not mark the account as warm or cold. + /// It will not bump transition_id or return if it is cold or warm loaded. This function is useful + /// when we know account is warm, touched and already loaded. + #[inline] + pub fn load_account_mut_unsafe<'a, 'b, DB: Database>( + &'a mut self, + db: &'b mut DB, + address: Address, + ) -> Option> + where + 'b: 'a, + { + let account = self.state.get_mut(&address)?; + Some(JournaledAccount::new( + address, + account, + &mut self.journal, + db, + self.warm_addresses.access_list(), + self.transaction_id, + )) + } + /// Loads account. If account is already loaded it will be marked as warm. + #[inline(never)] + pub fn load_account_mut_optional<'a, 'b, DB: Database>( + &'a mut self, + db: &'b mut DB, + address: Address, + skip_cold_load: bool, + ) -> Result>, JournalLoadError> + where + 'b: 'a, + { + let (account, is_cold) = match self.state.entry(address) { Entry::Occupied(entry) => { let account = entry.into_mut(); // skip load if account is cold. let mut is_cold = account.is_cold_transaction_id(self.transaction_id); - if is_cold { - // account can be loaded by we still need to check warm_addresses to see if it is cold. - let should_be_cold = self.warm_addresses.is_cold(&address); - // dont load it cold if skipping cold load is true. - if should_be_cold && skip_cold_load { - return Err(JournalLoadError::ColdLoadSkipped); - } - is_cold = should_be_cold; + if unlikely(is_cold) { + is_cold = self + .warm_addresses + .check_is_cold(&address, skip_cold_load)?; // mark it warm. account.mark_warm_with_transaction_id(self.transaction_id); @@ -690,56 +749,46 @@ impl JournalInner { } // unmark locally created account.unmark_created_locally(); + + // journal loading of cold account. + self.journal.push(ENTRY::account_warmed(address)); } - StateLoad { - data: account, - is_cold, - } + (account, is_cold) } Entry::Vacant(vac) => { - // Precompiles among some other account(coinbase included) are warm loaded so we need to take that into account - let is_cold = self.warm_addresses.is_cold(&address); + let is_cold = self + .warm_addresses + .check_is_cold(&address, skip_cold_load)?; - // dont load cold account if skip_cold_load is true - if is_cold && skip_cold_load { - return Err(JournalLoadError::ColdLoadSkipped); - } let account = if let Some(account) = db.basic(address)? { account.into() } else { Account::new_not_existing(self.transaction_id) }; - StateLoad { - data: vac.insert(account), - is_cold, + // journal loading of cold account. + if is_cold { + self.journal.push(ENTRY::account_warmed(address)); } + + (vac.insert(account), is_cold) } }; - // journal loading of cold account. - if load.is_cold { - self.journal.push(ENTRY::account_warmed(address)); - } - - if load_code && load.data.info.code.is_none() { - let info = &mut load.data.info; - let code = if info.code_hash == KECCAK_EMPTY { - Bytecode::default() - } else { - db.code_by_hash(info.code_hash)? - }; - info.code = Some(code); - } - - Ok(load.map(|i| JournaledAccount::new(address, i, &mut self.journal))) + Ok(StateLoad::new( + JournaledAccount::new( + address, + account, + &mut self.journal, + db, + self.warm_addresses.access_list(), + self.transaction_id, + ), + is_cold, + )) } /// Loads storage slot. - /// - /// # Panics - /// - /// Panics if the account is not present in the state. #[inline] pub fn sload( &mut self, @@ -748,46 +797,45 @@ impl JournalInner { key: StorageKey, skip_cold_load: bool, ) -> Result, JournalLoadError> { - // assume acc is warm - let account = self.state.get_mut(&address).unwrap(); - - let is_newly_created = account.is_created(); - let (value, is_cold) = match account.storage.entry(key) { - Entry::Occupied(occ) => { - let slot = occ.into_mut(); - // skip load if account is cold. - let is_cold = slot.is_cold_transaction_id(self.transaction_id); - if skip_cold_load && is_cold { - return Err(JournalLoadError::ColdLoadSkipped); - } - slot.mark_warm_with_transaction_id(self.transaction_id); - (slot.present_value, is_cold) - } - Entry::Vacant(vac) => { - // is storage cold - let is_cold = !self.warm_addresses.is_storage_warm(&address, &key); - - if is_cold && skip_cold_load { - return Err(JournalLoadError::ColdLoadSkipped); - } - // if storage was cleared, we don't need to ping db. - let value = if is_newly_created { - StorageValue::ZERO - } else { - db.storage(address, key)? - }; - vac.insert(EvmStorageSlot::new(value, self.transaction_id)); + self.load_account_mut(db, address)? + .sload(key, skip_cold_load) + .map(|s| s.map(|s| s.present_value)) + } - (value, is_cold) - } + /// Loads storage slot. + /// + /// If account is not present it will return [`JournalLoadError::ColdLoadSkipped`] error. + #[inline] + pub fn sload_unsafe( + &mut self, + db: &mut DB, + address: Address, + key: StorageKey, + skip_cold_load: bool, + ) -> Result, JournalLoadError> { + let Some(mut account) = self.load_account_mut_unsafe(db, address) else { + return Err(JournalLoadError::ColdLoadSkipped); }; - if is_cold { - // add it to journal as cold loaded. - self.journal.push(ENTRY::storage_warmed(address, key)); - } + account + .sload(key, skip_cold_load) + .map(|s| s.map(|s| s.present_value)) + } - Ok(StateLoad::new(value, is_cold)) + /// Stores storage slot. + /// + /// If account is not present it will load from database + #[inline] + pub fn sstore( + &mut self, + db: &mut DB, + address: Address, + key: StorageKey, + new: StorageValue, + skip_cold_load: bool, + ) -> Result, JournalLoadError> { + self.load_account_mut(db, address)? + .sstore(key, new, skip_cold_load) } /// Stores storage slot. @@ -796,7 +844,7 @@ impl JournalInner { /// /// **Note**: Account should already be present in our state. #[inline] - pub fn sstore( + pub fn sstore_unsafe( &mut self, db: &mut DB, address: Address, @@ -804,37 +852,11 @@ impl JournalInner { new: StorageValue, skip_cold_load: bool, ) -> Result, JournalLoadError> { - // assume that acc exists and load the slot. - let present = self.sload(db, address, key, skip_cold_load)?; - let acc = self.state.get_mut(&address).unwrap(); - - // if there is no original value in dirty return present value, that is our original. - let slot = acc.storage.get_mut(&key).unwrap(); - - // new value is same as present, we don't need to do anything - if present.data == new { - return Ok(StateLoad::new( - SStoreResult { - original_value: slot.original_value(), - present_value: present.data, - new_value: new, - }, - present.is_cold, - )); - } + let Some(mut account) = self.load_account_mut_unsafe(db, address) else { + return Err(JournalLoadError::ColdLoadSkipped); + }; - self.journal - .push(ENTRY::storage_changed(address, key, present.data)); - // insert value into present state. - slot.present_value = new; - Ok(StateLoad::new( - SStoreResult { - original_value: slot.original_value(), - present_value: present.data, - new_value: new, - }, - present.is_cold, - )) + account.sstore(key, new, skip_cold_load) } /// Read transient storage tied to the account. @@ -925,7 +947,7 @@ mod tests { // Try to sload with skip_cold_load=true - should succeed because slot is in access list let mut db = EmptyDB::new(); - let result = journal.sload(&mut db, test_address, test_key, true); + let result = journal.sload_unsafe(&mut db, test_address, test_key, true); // Should succeed and return as warm assert!(result.is_ok()); diff --git a/crates/context/src/journal/warm_addresses.rs b/crates/context/src/journal/warm_addresses.rs index ac5f1a6a40..27e5bce936 100644 --- a/crates/context/src/journal/warm_addresses.rs +++ b/crates/context/src/journal/warm_addresses.rs @@ -3,6 +3,7 @@ //! It is used to optimize access to precompile addresses. use bitvec::{bitvec, vec::BitVec}; +use context_interface::journaled_state::JournalLoadError; use primitives::{short_address, Address, HashMap, HashSet, StorageKey, SHORT_ADDRESS_CAP}; /// Stores addresses that are warm loaded. Contains precompiles and coinbase address. @@ -91,6 +92,12 @@ impl WarmAddresses { self.access_list = access_list; } + /// Returns the access list. + #[inline] + pub fn access_list(&self) -> &HashMap> { + &self.access_list + } + /// Clear the coinbase address. #[inline] pub fn clear_coinbase(&mut self) { @@ -150,6 +157,22 @@ impl WarmAddresses { pub fn is_cold(&self, address: &Address) -> bool { !self.is_warm(address) } + + /// Checks if the address is cold loaded and returns an error if it is and skip_cold_load is true. + #[inline(never)] + pub fn check_is_cold( + &self, + address: &Address, + skip_cold_load: bool, + ) -> Result> { + let is_cold = self.is_cold(address); + + if is_cold && skip_cold_load { + return Err(JournalLoadError::ColdLoadSkipped); + } + + Ok(is_cold) + } } #[cfg(test)] diff --git a/crates/handler/src/frame.rs b/crates/handler/src/frame.rs index aeecddd1a3..00e8b99e4a 100644 --- a/crates/handler/src/frame.rs +++ b/crates/handler/src/frame.rs @@ -5,7 +5,7 @@ use crate::{ use context::result::FromStringError; use context_interface::{ context::ContextError, - journaled_state::{JournalCheckpoint, JournalTr}, + journaled_state::{account::JournaledAccountTr, JournalCheckpoint, JournalTr}, local::{FrameToken, OutFrame}, Cfg, ContextTr, Database, }; @@ -276,7 +276,8 @@ impl EthFrame { } // Fetch balance of caller. - let mut caller_info = context.journal_mut().load_account_mut(inputs.caller)?; + let journal = context.journal_mut(); + let mut caller_info = journal.load_account_mut(inputs.caller)?; // Check if caller has enough balance to send to the created contract. // decrement of balance is done in the create_account_checkpoint. @@ -301,8 +302,10 @@ impl EthFrame { CreateScheme::Custom { address } => address, }; + drop(caller_info); // drop the loaded account to avoid borrow checker issues. + // warm load account. - context.journal_mut().load_account(created_address)?; + journal.load_account(created_address)?; // Create account, transfer funds and make the journal checkpoint. let checkpoint = match context.journal_mut().create_account_checkpoint( diff --git a/crates/handler/src/post_execution.rs b/crates/handler/src/post_execution.rs index bd0471d717..b5a8bef35d 100644 --- a/crates/handler/src/post_execution.rs +++ b/crates/handler/src/post_execution.rs @@ -1,4 +1,5 @@ use crate::FrameResult; +use context::journaled_state::account::JournaledAccountTr; use context_interface::{ journaled_state::JournalTr, result::{ExecutionResult, HaltReason, HaltReasonTr}, diff --git a/crates/handler/src/pre_execution.rs b/crates/handler/src/pre_execution.rs index 6d916083a2..dde3424759 100644 --- a/crates/handler/src/pre_execution.rs +++ b/crates/handler/src/pre_execution.rs @@ -5,7 +5,7 @@ use crate::{EvmTr, PrecompileProvider}; use bytecode::Bytecode; use context_interface::{ - journaled_state::JournalTr, + journaled_state::{account::JournaledAccountTr, JournalTr}, result::InvalidTransaction, transaction::{AccessListItemTr, AuthorizationTr, Transaction, TransactionType}, Block, Cfg, ContextTr, Database, diff --git a/crates/op-revm/src/handler.rs b/crates/op-revm/src/handler.rs index f444e6f420..75dae9c2a5 100644 --- a/crates/op-revm/src/handler.rs +++ b/crates/op-revm/src/handler.rs @@ -6,7 +6,11 @@ use crate::{ L1BlockInfo, OpHaltReason, OpSpecId, }; use revm::{ - context::{journaled_state::JournalCheckpoint, result::InvalidTransaction, LocalContextTr}, + context::{ + journaled_state::{account::JournaledAccountTr, JournalCheckpoint}, + result::InvalidTransaction, + LocalContextTr, + }, context_interface::{ context::ContextError, result::{EVMError, ExecutionResult, FromStringError}, @@ -404,6 +408,8 @@ where acc.bump_nonce(); acc.incr_balance(U256::from(mint.unwrap_or_default())); + drop(acc); // drop the loaded account to avoid borrow checker issues. + // We can now commit the changes. journal.commit_tx(); diff --git a/crates/primitives/src/hints_util.rs b/crates/primitives/src/hints_util.rs new file mode 100644 index 0000000000..dc858e9d5f --- /dev/null +++ b/crates/primitives/src/hints_util.rs @@ -0,0 +1,32 @@ +//! Utility functions for hints. +//! Used from Hashbrown . + +// FIXME: Replace with `core::hint::{likely, unlikely}` once they are stable. +// pub use core::intrinsics::{likely, unlikely}; + +/// Cold path function. +#[inline(always)] +#[cold] +pub fn cold_path() {} + +/// Returns `b` but mark `false` path as cold +#[inline(always)] +pub fn likely(b: bool) -> bool { + if b { + true + } else { + cold_path(); + false + } +} + +/// Returns `b` but mark `true` path as cold +#[inline(always)] +pub fn unlikely(b: bool) -> bool { + if b { + cold_path(); + true + } else { + false + } +} diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index 5f1b2e112b..9193733bb2 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -24,6 +24,7 @@ pub mod eip7823; pub mod eip7825; pub mod eip7907; pub mod hardfork; +pub mod hints_util; mod once_lock; pub use constants::*; diff --git a/examples/cheatcode_inspector/src/main.rs b/examples/cheatcode_inspector/src/main.rs index a40b0c5ca3..bd4ec43a27 100644 --- a/examples/cheatcode_inspector/src/main.rs +++ b/examples/cheatcode_inspector/src/main.rs @@ -8,7 +8,7 @@ use revm::{ context::{ - journaled_state::{AccountInfoLoad, JournalLoadError}, + journaled_state::{account::JournaledAccount, AccountInfoLoad, JournalLoadError}, result::InvalidTransaction, BlockEnv, Cfg, CfgEnv, ContextTr, Evm, LocalContext, TxEnv, }, @@ -60,7 +60,7 @@ impl Backend { impl JournalTr for Backend { type Database = InMemoryDB; type State = EvmState; - type JournalEntry = JournalEntry; + type JournaledAccount<'a> = JournaledAccount<'a, JournalEntry, Self::Database>; fn new(database: InMemoryDB) -> Self { Self::new(SpecId::default(), database) @@ -307,9 +307,7 @@ impl JournalTr for Backend { address: Address, load_code: bool, ) -> Result< - StateLoad< - revm::context::journaled_state::account::JournaledAccount<'_, Self::JournalEntry>, - >, + StateLoad>, ::Error, > { self.journaled_state diff --git a/examples/erc20_gas/src/handler.rs b/examples/erc20_gas/src/handler.rs index b1ff47d890..66f961a678 100644 --- a/examples/erc20_gas/src/handler.rs +++ b/examples/erc20_gas/src/handler.rs @@ -1,5 +1,5 @@ use revm::{ - context::Cfg, + context::{journaled_state::account::JournaledAccountTr, Cfg}, context_interface::{result::HaltReason, Block, ContextTr, JournalTr, Transaction}, handler::{ pre_execution::{calculate_caller_fee, validate_account_nonce_and_code_with_components}, @@ -62,6 +62,8 @@ where caller_account.bump_nonce(); } + drop(caller_account); // drop the loaded account to avoid borrow checker issues. + let account_balance_slot = erc_address_storage(tx.caller()); // load account balance