11//! Execution cache implementation for block processing.
2- use alloy_primitives:: { Address , StorageKey , StorageValue , B256 } ;
2+ use alloy_primitives:: {
3+ map:: { DefaultHashBuilder , HashSet } ,
4+ Address , StorageKey , StorageValue , B256 ,
5+ } ;
36use metrics:: Gauge ;
47use mini_moka:: sync:: CacheBuilder ;
58use reth_errors:: ProviderResult ;
@@ -14,7 +17,6 @@ use reth_trie::{
1417 updates:: TrieUpdates , AccountProof , HashedPostState , HashedStorage , MultiProof ,
1518 MultiProofTargets , StorageMultiProof , StorageProof , TrieInput ,
1619} ;
17- use revm_primitives:: map:: DefaultHashBuilder ;
1820use std:: { sync:: Arc , time:: Duration } ;
1921use tracing:: { debug_span, instrument, trace} ;
2022
@@ -300,65 +302,70 @@ pub(crate) struct ExecutionCache {
300302 /// Cache for contract bytecode, keyed by code hash.
301303 code_cache : Cache < B256 , Option < Bytecode > > ,
302304
303- /// Per-account storage cache: outer cache keyed by Address, inner cache tracks that account’s
304- /// storage slots .
305- storage_cache : Cache < Address , AccountStorageCache > ,
305+ /// Flattened storage cache: composite key of (` Address`, `StorageKey`) maps directly to
306+ /// values .
307+ storage_cache : Cache < ( Address , StorageKey ) , Option < StorageValue > > ,
306308
307309 /// Cache for basic account information (nonce, balance, code hash).
308310 account_cache : Cache < Address , Option < Account > > ,
309311}
310312
311313impl ExecutionCache {
312- /// Get storage value from hierarchical cache.
314+ /// Get storage value from flattened cache.
313315 ///
314316 /// Returns a `SlotStatus` indicating whether:
315- /// - `NotCached`: The account's storage cache doesn't exist
316- /// - `Empty`: The slot exists in the account's cache but is empty
317+ /// - `NotCached`: The storage slot is not in the cache
318+ /// - `Empty`: The slot exists in the cache but is empty
317319 /// - `Value`: The slot exists and has a specific value
318320 pub ( crate ) fn get_storage ( & self , address : & Address , key : & StorageKey ) -> SlotStatus {
319- match self . storage_cache . get ( address) {
321+ match self . storage_cache . get ( & ( * address, * key ) ) {
320322 None => SlotStatus :: NotCached ,
321- Some ( account_cache) => account_cache. get_storage ( key) ,
323+ Some ( None ) => SlotStatus :: Empty ,
324+ Some ( Some ( value) ) => SlotStatus :: Value ( value) ,
322325 }
323326 }
324327
325- /// Insert storage value into hierarchical cache
328+ /// Insert storage value into flattened cache
326329 pub ( crate ) fn insert_storage (
327330 & self ,
328331 address : Address ,
329332 key : StorageKey ,
330333 value : Option < StorageValue > ,
331334 ) {
332- self . insert_storage_bulk ( address, [ ( key, value) ] ) ;
335+ self . storage_cache . insert ( ( address, key) , value) ;
333336 }
334337
335- /// Insert multiple storage values into hierarchical cache for a single account
338+ /// Insert multiple storage values into flattened cache for a single account
336339 ///
337- /// This method is optimized for inserting multiple storage values for the same address
338- /// by doing the account cache lookup only once instead of for each key-value pair .
340+ /// This method inserts multiple storage values for the same address directly
341+ /// into the flattened cache.
339342 pub ( crate ) fn insert_storage_bulk < I > ( & self , address : Address , storage_entries : I )
340343 where
341344 I : IntoIterator < Item = ( StorageKey , Option < StorageValue > ) > ,
342345 {
343- let account_cache = self . storage_cache . get ( & address) . unwrap_or_else ( || {
344- let account_cache = AccountStorageCache :: default ( ) ;
345- self . storage_cache . insert ( address, account_cache. clone ( ) ) ;
346- account_cache
347- } ) ;
348-
349346 for ( key, value) in storage_entries {
350- account_cache . insert_storage ( key, value) ;
347+ self . storage_cache . insert ( ( address , key) , value) ;
351348 }
352349 }
353350
354- /// Invalidate storage for specific account
355- pub ( crate ) fn invalidate_account_storage ( & self , address : & Address ) {
356- self . storage_cache . invalidate ( address) ;
357- }
358-
359351 /// Returns the total number of storage slots cached across all accounts
360352 pub ( crate ) fn total_storage_slots ( & self ) -> usize {
361- self . storage_cache . iter ( ) . map ( |addr| addr. len ( ) ) . sum ( )
353+ self . storage_cache . entry_count ( ) as usize
354+ }
355+
356+ /// Invalidates the storage for all addresses in the set
357+ #[ instrument( level = "debug" , target = "engine::caching" , skip_all, fields( accounts = addresses. len( ) ) ) ]
358+ pub ( crate ) fn invalidate_storages ( & self , addresses : HashSet < & Address > ) {
359+ // NOTE: this must collect because the invalidate function should not be called while we
360+ // hold an iter for it
361+ let storage_entries = self
362+ . storage_cache
363+ . iter ( )
364+ . filter_map ( |entry| addresses. contains ( & entry. key ( ) . 0 ) . then_some ( * entry. key ( ) ) )
365+ . collect :: < Vec < _ > > ( ) ;
366+ for key in storage_entries {
367+ self . storage_cache . invalidate ( & key)
368+ }
362369 }
363370
364371 /// Inserts the post-execution state changes into the cache.
@@ -398,6 +405,7 @@ impl ExecutionCache {
398405 state_updates. state. values( ) . map( |account| account. storage. len( ) ) . sum:: <usize >( )
399406 )
400407 . entered ( ) ;
408+ let mut invalidated_accounts = HashSet :: default ( ) ;
401409 for ( addr, account) in & state_updates. state {
402410 // If the account was not modified, as in not changed and not destroyed, then we have
403411 // nothing to do w.r.t. this particular account and can move on
@@ -410,7 +418,7 @@ impl ExecutionCache {
410418 // Invalidate the account cache entry if destroyed
411419 self . account_cache . invalidate ( addr) ;
412420
413- self . invalidate_account_storage ( addr) ;
421+ invalidated_accounts . insert ( addr) ;
414422 continue
415423 }
416424
@@ -437,6 +445,9 @@ impl ExecutionCache {
437445 self . account_cache . insert ( * addr, Some ( Account :: from ( account_info) ) ) ;
438446 }
439447
448+ // invalidate storage for all destroyed accounts
449+ self . invalidate_storages ( invalidated_accounts) ;
450+
440451 Ok ( ( ) )
441452 }
442453}
@@ -465,11 +476,11 @@ impl ExecutionCacheBuilder {
465476 const TIME_TO_IDLE : Duration = Duration :: from_secs ( 3600 ) ; // 1 hour
466477
467478 let storage_cache = CacheBuilder :: new ( self . storage_cache_entries )
468- . weigher ( |_key : & Address , value : & AccountStorageCache | -> u32 {
469- // values based on results from measure_storage_cache_overhead test
470- let base_weight = 39_000 ;
471- let slots_weight = value . len ( ) * 218 ;
472- ( base_weight + slots_weight ) as u32
479+ . weigher ( |_key : & ( Address , StorageKey ) , _value : & Option < StorageValue > | -> u32 {
480+ // Size of composite key (Address + StorageKey) + Option<StorageValue>
481+ // Address: 20 bytes, StorageKey: 32 bytes, Option<StorageValue>: 33 bytes
482+ // Plus some overhead for the hash map entry
483+ 120_u32
473484 } )
474485 . max_capacity ( storage_cache_size)
475486 . time_to_live ( EXPIRY_TIME )
@@ -592,56 +603,6 @@ impl SavedCache {
592603 }
593604}
594605
595- /// Cache for an individual account's storage slots.
596- ///
597- /// This represents the second level of the hierarchical storage cache.
598- /// Each account gets its own `AccountStorageCache` to store accessed storage slots.
599- #[ derive( Debug , Clone ) ]
600- pub ( crate ) struct AccountStorageCache {
601- /// Map of storage keys to their cached values.
602- slots : Cache < StorageKey , Option < StorageValue > > ,
603- }
604-
605- impl AccountStorageCache {
606- /// Create a new [`AccountStorageCache`]
607- pub ( crate ) fn new ( max_slots : u64 ) -> Self {
608- Self {
609- slots : CacheBuilder :: new ( max_slots) . build_with_hasher ( DefaultHashBuilder :: default ( ) ) ,
610- }
611- }
612-
613- /// Get a storage value from this account's cache.
614- /// - `NotCached`: The slot is not in the cache
615- /// - `Empty`: The slot is empty
616- /// - `Value`: The slot has a specific value
617- pub ( crate ) fn get_storage ( & self , key : & StorageKey ) -> SlotStatus {
618- match self . slots . get ( key) {
619- None => SlotStatus :: NotCached ,
620- Some ( None ) => SlotStatus :: Empty ,
621- Some ( Some ( value) ) => SlotStatus :: Value ( value) ,
622- }
623- }
624-
625- /// Insert a storage value
626- pub ( crate ) fn insert_storage ( & self , key : StorageKey , value : Option < StorageValue > ) {
627- self . slots . insert ( key, value) ;
628- }
629-
630- /// Returns the number of slots in the cache
631- pub ( crate ) fn len ( & self ) -> usize {
632- self . slots . entry_count ( ) as usize
633- }
634- }
635-
636- impl Default for AccountStorageCache {
637- fn default ( ) -> Self {
638- // With weigher and max_capacity in place, this number represents
639- // the maximum number of entries that can be stored, not the actual
640- // memory usage which is controlled by storage cache's max_capacity.
641- Self :: new ( 1_000_000 )
642- }
643- }
644-
645606#[ cfg( test) ]
646607mod tests {
647608 use super :: * ;
@@ -716,32 +677,36 @@ mod tests {
716677
717678 #[ test]
718679 fn measure_storage_cache_overhead ( ) {
719- let ( base_overhead, cache) = measure_allocation ( || AccountStorageCache :: new ( 1000 ) ) ;
720- println ! ( "Base AccountStorageCache overhead: {base_overhead} bytes" ) ;
680+ let ( base_overhead, cache) =
681+ measure_allocation ( || ExecutionCacheBuilder :: default ( ) . build_caches ( 1000 ) ) ;
682+ println ! ( "Base ExecutionCache overhead: {base_overhead} bytes" ) ;
721683 let mut rng = rand:: rng ( ) ;
722684
685+ let address = Address :: random ( ) ;
723686 let key = StorageKey :: random ( ) ;
724687 let value = StorageValue :: from ( rng. random :: < u128 > ( ) ) ;
725688 let ( first_slot, _) = measure_allocation ( || {
726- cache. insert_storage ( key, Some ( value) ) ;
689+ cache. insert_storage ( address , key, Some ( value) ) ;
727690 } ) ;
728691 println ! ( "First slot insertion overhead: {first_slot} bytes" ) ;
729692
730693 const TOTAL_SLOTS : usize = 10_000 ;
731694 let ( test_slots, _) = measure_allocation ( || {
732695 for _ in 0 ..TOTAL_SLOTS {
696+ let addr = Address :: random ( ) ;
733697 let key = StorageKey :: random ( ) ;
734698 let value = StorageValue :: from ( rng. random :: < u128 > ( ) ) ;
735- cache. insert_storage ( key, Some ( value) ) ;
699+ cache. insert_storage ( addr , key, Some ( value) ) ;
736700 }
737701 } ) ;
738702 println ! ( "Average overhead over {} slots: {} bytes" , TOTAL_SLOTS , test_slots / TOTAL_SLOTS ) ;
739703
740704 println ! ( "\n Theoretical sizes:" ) ;
705+ println ! ( "Address size: {} bytes" , size_of:: <Address >( ) ) ;
741706 println ! ( "StorageKey size: {} bytes" , size_of:: <StorageKey >( ) ) ;
742707 println ! ( "StorageValue size: {} bytes" , size_of:: <StorageValue >( ) ) ;
743708 println ! ( "Option<StorageValue> size: {} bytes" , size_of:: <Option <StorageValue >>( ) ) ;
744- println ! ( "Option<B256> size: {} bytes" , size_of:: <Option < B256 > >( ) ) ;
709+ println ! ( "(Address, StorageKey) size: {} bytes" , size_of:: <( Address , StorageKey ) >( ) ) ;
745710 }
746711
747712 #[ test]
0 commit comments