1+ use alloy_eip7928:: BAL_RETENTION_PERIOD_SLOTS ;
12use alloy_primitives:: { BlockHash , BlockNumber , Bytes } ;
23use parking_lot:: RwLock ;
4+ use reth_prune_types:: PruneMode ;
35use reth_storage_api:: { BalStore , GetBlockAccessListLimit } ;
46use reth_storage_errors:: provider:: ProviderResult ;
5- use std:: { collections:: HashMap , sync:: Arc } ;
7+ use std:: {
8+ collections:: { BTreeMap , HashMap } ,
9+ sync:: Arc ,
10+ } ;
611
712/// Basic in-memory BAL store keyed by block hash.
8- #[ derive( Debug , Clone , Default ) ]
13+ #[ derive( Debug , Clone ) ]
914pub struct InMemoryBalStore {
10- entries : Arc < RwLock < HashMap < BlockHash , Bytes > > > ,
15+ config : BalConfig ,
16+ inner : Arc < RwLock < InMemoryBalStoreInner > > ,
17+ }
18+
19+ impl InMemoryBalStore {
20+ /// Creates a new in-memory BAL store with the given config.
21+ pub fn new ( config : BalConfig ) -> Self {
22+ Self { config, inner : Arc :: new ( RwLock :: new ( InMemoryBalStoreInner :: default ( ) ) ) }
23+ }
24+ }
25+
26+ impl Default for InMemoryBalStore {
27+ fn default ( ) -> Self {
28+ Self :: new ( BalConfig :: default ( ) )
29+ }
30+ }
31+
32+ /// Configuration for BAL storage.
33+ #[ derive( Debug , Clone , Copy , Eq , PartialEq ) ]
34+ pub struct BalConfig {
35+ /// Retention policy for BALs kept in memory.
36+ in_memory_retention : Option < PruneMode > ,
37+ }
38+
39+ impl BalConfig {
40+ /// Returns a config with no in-memory BAL retention limit.
41+ pub const fn unbounded ( ) -> Self {
42+ Self { in_memory_retention : None }
43+ }
44+
45+ /// Returns a config with the given in-memory BAL retention policy.
46+ pub const fn with_in_memory_retention ( in_memory_retention : PruneMode ) -> Self {
47+ Self { in_memory_retention : Some ( in_memory_retention) }
48+ }
49+ }
50+
51+ impl Default for BalConfig {
52+ fn default ( ) -> Self {
53+ Self :: with_in_memory_retention ( PruneMode :: Distance ( BAL_RETENTION_PERIOD_SLOTS ) )
54+ }
55+ }
56+
57+ #[ derive( Debug , Default ) ]
58+ struct InMemoryBalStoreInner {
59+ entries : HashMap < BlockHash , BalEntry > ,
60+ hashes_by_number : BTreeMap < BlockNumber , Vec < BlockHash > > ,
61+ highest_block_number : Option < BlockNumber > ,
62+ }
63+
64+ impl InMemoryBalStoreInner {
65+ // Inserts a BAL and keeps the block-number index in sync.
66+ fn insert ( & mut self , block_hash : BlockHash , block_number : BlockNumber , bal : Bytes ) {
67+ let empty_block_number =
68+ self . entries . insert ( block_hash, BalEntry { block_number, bal } ) . and_then ( |entry| {
69+ let hashes = self . hashes_by_number . get_mut ( & entry. block_number ) ?;
70+ hashes. retain ( |hash| * hash != block_hash) ;
71+ hashes. is_empty ( ) . then_some ( entry. block_number )
72+ } ) ;
73+
74+ if let Some ( block_number) = empty_block_number {
75+ self . hashes_by_number . remove ( & block_number) ;
76+ }
77+
78+ self . hashes_by_number . entry ( block_number) . or_default ( ) . push ( block_hash) ;
79+ self . highest_block_number = Some (
80+ self . highest_block_number . map_or ( block_number, |highest| highest. max ( block_number) ) ,
81+ ) ;
82+ }
83+
84+ // Removes BALs outside the configured retention window.
85+ fn prune ( & mut self , prune_mode : Option < PruneMode > ) {
86+ let Some ( prune_mode) = prune_mode else { return } ;
87+ let Some ( tip) = self . highest_block_number else { return } ;
88+
89+ while let Some ( ( & block_number, _) ) = self . hashes_by_number . first_key_value ( ) {
90+ if !prune_mode. should_prune ( block_number, tip) {
91+ break
92+ }
93+
94+ let Some ( ( _, hashes) ) = self . hashes_by_number . pop_first ( ) else { break } ;
95+ for hash in hashes {
96+ self . entries . remove ( & hash) ;
97+ }
98+ }
99+ }
100+ }
101+
102+ #[ derive( Debug ) ]
103+ struct BalEntry {
104+ block_number : BlockNumber ,
105+ bal : Bytes ,
11106}
12107
13108impl BalStore for InMemoryBalStore {
14109 fn insert (
15110 & self ,
16111 block_hash : BlockHash ,
17- _block_number : BlockNumber ,
112+ block_number : BlockNumber ,
18113 bal : Bytes ,
19114 ) -> ProviderResult < ( ) > {
20- self . entries . write ( ) . insert ( block_hash, bal) ;
115+ let mut inner = self . inner . write ( ) ;
116+ inner. insert ( block_hash, block_number, bal) ;
117+ inner. prune ( self . config . in_memory_retention ) ;
21118 Ok ( ( ) )
22119 }
23120
24121 fn get_by_hashes ( & self , block_hashes : & [ BlockHash ] ) -> ProviderResult < Vec < Option < Bytes > > > {
25- let entries = self . entries . read ( ) ;
122+ let inner = self . inner . read ( ) ;
26123 let mut result = Vec :: with_capacity ( block_hashes. len ( ) ) ;
27124
28125 for hash in block_hashes {
29- result. push ( entries. get ( hash) . cloned ( ) ) ;
126+ result. push ( inner . entries . get ( hash) . map ( |entry| entry . bal . clone ( ) ) ) ;
30127 }
31128
32129 Ok ( result)
@@ -38,11 +135,15 @@ impl BalStore for InMemoryBalStore {
38135 limit : GetBlockAccessListLimit ,
39136 out : & mut Vec < Bytes > ,
40137 ) -> ProviderResult < ( ) > {
41- let entries = self . entries . read ( ) ;
138+ let inner = self . inner . read ( ) ;
42139 let mut size = 0 ;
43140
44141 for hash in block_hashes {
45- let bal = entries. get ( hash) . cloned ( ) . unwrap_or_else ( || Bytes :: from_static ( & [ 0xc0 ] ) ) ;
142+ let bal = inner
143+ . entries
144+ . get ( hash)
145+ . map ( |entry| entry. bal . clone ( ) )
146+ . unwrap_or_else ( || Bytes :: from_static ( & [ 0xc0 ] ) ) ;
46147 size += bal. len ( ) ;
47148 out. push ( bal) ;
48149
@@ -106,4 +207,54 @@ mod tests {
106207
107208 assert_eq ! ( limited, vec![ bal0, bal1] ) ;
108209 }
210+
211+ #[ test]
212+ fn default_retention_prunes_old_bals ( ) {
213+ let store = InMemoryBalStore :: default ( ) ;
214+ let old_hash = B256 :: random ( ) ;
215+ let retained_hash = B256 :: random ( ) ;
216+ let tip_hash = B256 :: random ( ) ;
217+ let old_bal = Bytes :: from_static ( b"old" ) ;
218+ let retained_bal = Bytes :: from_static ( b"retained" ) ;
219+ let tip_bal = Bytes :: from_static ( b"tip" ) ;
220+
221+ store. insert ( old_hash, 1 , old_bal) . unwrap ( ) ;
222+ store. insert ( retained_hash, BAL_RETENTION_PERIOD_SLOTS , retained_bal. clone ( ) ) . unwrap ( ) ;
223+ store. insert ( tip_hash, BAL_RETENTION_PERIOD_SLOTS + 2 , tip_bal. clone ( ) ) . unwrap ( ) ;
224+
225+ assert_eq ! (
226+ store. get_by_hashes( & [ old_hash, retained_hash, tip_hash] ) . unwrap( ) ,
227+ vec![ None , Some ( retained_bal) , Some ( tip_bal) ]
228+ ) ;
229+ }
230+
231+ #[ test]
232+ fn unbounded_retention_keeps_old_bals ( ) {
233+ let store = InMemoryBalStore :: new ( BalConfig :: unbounded ( ) ) ;
234+ let old_hash = B256 :: random ( ) ;
235+ let tip_hash = B256 :: random ( ) ;
236+ let old_bal = Bytes :: from_static ( b"old" ) ;
237+ let tip_bal = Bytes :: from_static ( b"tip" ) ;
238+
239+ store. insert ( old_hash, 1 , old_bal. clone ( ) ) . unwrap ( ) ;
240+ store. insert ( tip_hash, BAL_RETENTION_PERIOD_SLOTS + 1 , tip_bal. clone ( ) ) . unwrap ( ) ;
241+
242+ assert_eq ! (
243+ store. get_by_hashes( & [ old_hash, tip_hash] ) . unwrap( ) ,
244+ vec![ Some ( old_bal) , Some ( tip_bal) ]
245+ ) ;
246+ }
247+
248+ #[ test]
249+ fn reinserting_hash_updates_number_index ( ) {
250+ let store =
251+ InMemoryBalStore :: new ( BalConfig :: with_in_memory_retention ( PruneMode :: Before ( 2 ) ) ) ;
252+ let hash = B256 :: random ( ) ;
253+ let bal = Bytes :: from_static ( b"bal" ) ;
254+
255+ store. insert ( hash, 1 , Bytes :: from_static ( b"old" ) ) . unwrap ( ) ;
256+ store. insert ( hash, 2 , bal. clone ( ) ) . unwrap ( ) ;
257+
258+ assert_eq ! ( store. get_by_hashes( & [ hash] ) . unwrap( ) , vec![ Some ( bal) ] ) ;
259+ }
109260}
0 commit comments