11//! Execution cache implementation for block processing.
2- use alloy_primitives:: {
3- map:: { DefaultHashBuilder , HashSet } ,
4- Address , StorageKey , StorageValue , B256 ,
5- } ;
2+ use alloy_primitives:: { Address , StorageKey , StorageValue , B256 } ;
63use metrics:: Gauge ;
74use mini_moka:: sync:: CacheBuilder ;
85use reth_errors:: ProviderResult ;
@@ -17,6 +14,7 @@ use reth_trie::{
1714 updates:: TrieUpdates , AccountProof , HashedPostState , HashedStorage , MultiProof ,
1815 MultiProofTargets , StorageMultiProof , StorageProof , TrieInput ,
1916} ;
17+ use revm_primitives:: map:: DefaultHashBuilder ;
2018use std:: { sync:: Arc , time:: Duration } ;
2119use tracing:: { debug_span, instrument, trace} ;
2220
@@ -302,70 +300,65 @@ pub(crate) struct ExecutionCache {
302300 /// Cache for contract bytecode, keyed by code hash.
303301 code_cache : Cache < B256 , Option < Bytecode > > ,
304302
305- /// Flattened storage cache: composite key of (` Address`, `StorageKey`) maps directly to
306- /// values .
307- storage_cache : Cache < ( Address , StorageKey ) , Option < StorageValue > > ,
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 > ,
308306
309307 /// Cache for basic account information (nonce, balance, code hash).
310308 account_cache : Cache < Address , Option < Account > > ,
311309}
312310
313311impl ExecutionCache {
314- /// Get storage value from flattened cache.
312+ /// Get storage value from hierarchical cache.
315313 ///
316314 /// Returns a `SlotStatus` indicating whether:
317- /// - `NotCached`: The storage slot is not in the cache
318- /// - `Empty`: The slot exists in the cache but is empty
315+ /// - `NotCached`: The account's storage cache doesn't exist
316+ /// - `Empty`: The slot exists in the account's cache but is empty
319317 /// - `Value`: The slot exists and has a specific value
320318 pub ( crate ) fn get_storage ( & self , address : & Address , key : & StorageKey ) -> SlotStatus {
321- match self . storage_cache . get ( & ( * address, * key ) ) {
319+ match self . storage_cache . get ( address) {
322320 None => SlotStatus :: NotCached ,
323- Some ( None ) => SlotStatus :: Empty ,
324- Some ( Some ( value) ) => SlotStatus :: Value ( value) ,
321+ Some ( account_cache) => account_cache. get_storage ( key) ,
325322 }
326323 }
327324
328- /// Insert storage value into flattened cache
325+ /// Insert storage value into hierarchical cache
329326 pub ( crate ) fn insert_storage (
330327 & self ,
331328 address : Address ,
332329 key : StorageKey ,
333330 value : Option < StorageValue > ,
334331 ) {
335- self . storage_cache . insert ( ( address, key) , value) ;
332+ self . insert_storage_bulk ( address, [ ( key, value) ] ) ;
336333 }
337334
338- /// Insert multiple storage values into flattened cache for a single account
335+ /// Insert multiple storage values into hierarchical cache for a single account
339336 ///
340- /// This method inserts multiple storage values for the same address directly
341- /// into the flattened cache.
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 .
342339 pub ( crate ) fn insert_storage_bulk < I > ( & self , address : Address , storage_entries : I )
343340 where
344341 I : IntoIterator < Item = ( StorageKey , Option < StorageValue > ) > ,
345342 {
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+
346349 for ( key, value) in storage_entries {
347- self . storage_cache . insert ( ( address , key) , value) ;
350+ account_cache . insert_storage ( key, value) ;
348351 }
349352 }
350353
354+ /// Invalidate storage for specific account
355+ pub ( crate ) fn invalidate_account_storage ( & self , address : & Address ) {
356+ self . storage_cache . invalidate ( address) ;
357+ }
358+
351359 /// Returns the total number of storage slots cached across all accounts
352360 pub ( crate ) fn total_storage_slots ( & self ) -> usize {
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- }
361+ self . storage_cache . iter ( ) . map ( |addr| addr. len ( ) ) . sum ( )
369362 }
370363
371364 /// Inserts the post-execution state changes into the cache.
@@ -405,7 +398,6 @@ impl ExecutionCache {
405398 state_updates. state. values( ) . map( |account| account. storage. len( ) ) . sum:: <usize >( )
406399 )
407400 . entered ( ) ;
408- let mut invalidated_accounts = HashSet :: default ( ) ;
409401 for ( addr, account) in & state_updates. state {
410402 // If the account was not modified, as in not changed and not destroyed, then we have
411403 // nothing to do w.r.t. this particular account and can move on
@@ -418,7 +410,7 @@ impl ExecutionCache {
418410 // Invalidate the account cache entry if destroyed
419411 self . account_cache . invalidate ( addr) ;
420412
421- invalidated_accounts . insert ( addr) ;
413+ self . invalidate_account_storage ( addr) ;
422414 continue
423415 }
424416
@@ -445,9 +437,6 @@ impl ExecutionCache {
445437 self . account_cache . insert ( * addr, Some ( Account :: from ( account_info) ) ) ;
446438 }
447439
448- // invalidate storage for all destroyed accounts
449- self . invalidate_storages ( invalidated_accounts) ;
450-
451440 Ok ( ( ) )
452441 }
453442}
@@ -476,11 +465,11 @@ impl ExecutionCacheBuilder {
476465 const TIME_TO_IDLE : Duration = Duration :: from_secs ( 3600 ) ; // 1 hour
477466
478467 let storage_cache = CacheBuilder :: new ( self . storage_cache_entries )
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
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
484473 } )
485474 . max_capacity ( storage_cache_size)
486475 . time_to_live ( EXPIRY_TIME )
@@ -603,6 +592,56 @@ impl SavedCache {
603592 }
604593}
605594
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+
606645#[ cfg( test) ]
607646mod tests {
608647 use super :: * ;
@@ -677,36 +716,32 @@ mod tests {
677716
678717 #[ test]
679718 fn measure_storage_cache_overhead ( ) {
680- let ( base_overhead, cache) =
681- measure_allocation ( || ExecutionCacheBuilder :: default ( ) . build_caches ( 1000 ) ) ;
682- println ! ( "Base ExecutionCache overhead: {base_overhead} bytes" ) ;
719+ let ( base_overhead, cache) = measure_allocation ( || AccountStorageCache :: new ( 1000 ) ) ;
720+ println ! ( "Base AccountStorageCache overhead: {base_overhead} bytes" ) ;
683721 let mut rng = rand:: rng ( ) ;
684722
685- let address = Address :: random ( ) ;
686723 let key = StorageKey :: random ( ) ;
687724 let value = StorageValue :: from ( rng. random :: < u128 > ( ) ) ;
688725 let ( first_slot, _) = measure_allocation ( || {
689- cache. insert_storage ( address , key, Some ( value) ) ;
726+ cache. insert_storage ( key, Some ( value) ) ;
690727 } ) ;
691728 println ! ( "First slot insertion overhead: {first_slot} bytes" ) ;
692729
693730 const TOTAL_SLOTS : usize = 10_000 ;
694731 let ( test_slots, _) = measure_allocation ( || {
695732 for _ in 0 ..TOTAL_SLOTS {
696- let addr = Address :: random ( ) ;
697733 let key = StorageKey :: random ( ) ;
698734 let value = StorageValue :: from ( rng. random :: < u128 > ( ) ) ;
699- cache. insert_storage ( addr , key, Some ( value) ) ;
735+ cache. insert_storage ( key, Some ( value) ) ;
700736 }
701737 } ) ;
702738 println ! ( "Average overhead over {} slots: {} bytes" , TOTAL_SLOTS , test_slots / TOTAL_SLOTS ) ;
703739
704740 println ! ( "\n Theoretical sizes:" ) ;
705- println ! ( "Address size: {} bytes" , size_of:: <Address >( ) ) ;
706741 println ! ( "StorageKey size: {} bytes" , size_of:: <StorageKey >( ) ) ;
707742 println ! ( "StorageValue size: {} bytes" , size_of:: <StorageValue >( ) ) ;
708743 println ! ( "Option<StorageValue> size: {} bytes" , size_of:: <Option <StorageValue >>( ) ) ;
709- println ! ( "(Address, StorageKey) size: {} bytes" , size_of:: <( Address , StorageKey ) >( ) ) ;
744+ println ! ( "Option<B256> size: {} bytes" , size_of:: <Option < B256 > >( ) ) ;
710745 }
711746
712747 #[ test]
0 commit comments