1- use crate :: SimItem ;
1+ use crate :: { item:: SimIdentifier , CacheError , SimItem } ;
2+ use alloy:: consensus:: TxEnvelope ;
23use core:: fmt;
4+ use parking_lot:: RwLock ;
5+ use signet_bundle:: SignetEthBundle ;
36use std:: {
4- collections:: BTreeMap ,
5- sync:: { Arc , RwLock , RwLockWriteGuard } ,
7+ collections:: { BTreeMap , HashSet } ,
8+ sync:: Arc ,
69} ;
710
811/// A cache for the simulator.
912///
1013/// This cache is used to store the items that are being simulated.
1114#[ derive( Clone ) ]
1215pub struct SimCache {
13- inner : Arc < RwLock < BTreeMap < u128 , SimItem > > > ,
16+ inner : Arc < RwLock < CacheInner > > ,
1417 capacity : usize ,
1518}
1619
@@ -27,169 +30,273 @@ impl Default for SimCache {
2730}
2831
2932impl SimCache {
30- /// Create a new `SimCache` instance.
33+ /// Create a new `SimCache` instance, with a default capacity of `100` .
3134 pub fn new ( ) -> Self {
32- Self { inner : Arc :: new ( RwLock :: new ( BTreeMap :: new ( ) ) ) , capacity : 100 }
35+ Self { inner : Arc :: new ( RwLock :: new ( CacheInner :: new ( ) ) ) , capacity : 100 }
3336 }
3437
3538 /// Create a new `SimCache` instance with a given capacity.
3639 pub fn with_capacity ( capacity : usize ) -> Self {
37- Self { inner : Arc :: new ( RwLock :: new ( BTreeMap :: new ( ) ) ) , capacity }
40+ Self { inner : Arc :: new ( RwLock :: new ( CacheInner :: new ( ) ) ) , capacity }
3841 }
3942
4043 /// Get an iterator over the best items in the cache.
4144 pub fn read_best ( & self , n : usize ) -> Vec < ( u128 , SimItem ) > {
42- self . inner . read ( ) . unwrap ( ) . iter ( ) . rev ( ) . take ( n) . map ( |( k, v ) | ( * k, v . clone ( ) ) ) . collect ( )
45+ self . inner . read ( ) . items . iter ( ) . rev ( ) . take ( n) . map ( |( k, item ) | ( * k, item . clone ( ) ) ) . collect ( )
4346 }
4447
4548 /// Get the number of items in the cache.
4649 pub fn len ( & self ) -> usize {
47- self . inner . read ( ) . unwrap ( ) . len ( )
50+ self . inner . read ( ) . items . len ( )
4851 }
4952
5053 /// True if the cache is empty.
5154 pub fn is_empty ( & self ) -> bool {
52- self . inner . read ( ) . unwrap ( ) . is_empty ( )
55+ self . inner . read ( ) . items . is_empty ( )
5356 }
5457
5558 /// Get an item by key.
5659 pub fn get ( & self , key : u128 ) -> Option < SimItem > {
57- self . inner . read ( ) . unwrap ( ) . get ( & key) . cloned ( )
60+ self . inner . read ( ) . items . get ( & key) . cloned ( )
5861 }
5962
6063 /// Remove an item by key.
6164 pub fn remove ( & self , key : u128 ) -> Option < SimItem > {
62- self . inner . write ( ) . unwrap ( ) . remove ( & key)
65+ let mut inner = self . inner . write ( ) ;
66+ if let Some ( item) = inner. items . remove ( & key) {
67+ inner. seen . remove ( item. identifier ( ) . as_bytes ( ) ) ;
68+ Some ( item)
69+ } else {
70+ None
71+ }
6372 }
6473
65- fn add_inner (
66- guard : & mut RwLockWriteGuard < ' _ , BTreeMap < u128 , SimItem > > ,
67- mut score : u128 ,
68- item : SimItem ,
69- capacity : usize ,
70- ) {
74+ fn add_inner ( inner : & mut CacheInner , mut score : u128 , item : SimItem , capacity : usize ) {
75+ // Check if we've already seen this item - if so, don't add it
76+ if !inner . seen . insert ( item . identifier_owned ( ) ) {
77+ return ;
78+ }
79+
7180 // If it has the same score, we decrement (prioritizing earlier items)
72- while guard . contains_key ( & score) && score != 0 {
81+ while inner . items . contains_key ( & score) && score != 0 {
7382 score = score. saturating_sub ( 1 ) ;
7483 }
7584
76- if guard . len ( ) >= capacity {
85+ if inner . items . len ( ) >= capacity {
7786 // If we are at capacity, we need to remove the lowest score
78- guard. pop_first ( ) ;
87+ if let Some ( ( _, item) ) = inner. items . pop_first ( ) {
88+ inner. seen . remove ( & item. identifier_owned ( ) ) ;
89+ }
7990 }
8091
81- guard . entry ( score) . or_insert ( item ) ;
92+ inner . items . insert ( score, item . clone ( ) ) ;
8293 }
8394
84- /// Add an item to the cache.
85- ///
86- /// The basefee is used to calculate an estimated fee for the item.
87- pub fn add_item ( & self , item : impl Into < SimItem > , basefee : u64 ) {
88- let item = item. into ( ) ;
95+ /// Add a bundle to the cache.
96+ pub fn add_bundle ( & self , bundle : SignetEthBundle , basefee : u64 ) -> Result < ( ) , CacheError > {
97+ if bundle. replacement_uuid ( ) . is_none ( ) {
98+ // If the bundle does not have a replacement UUID, we cannot add it to the cache.
99+ return Err ( CacheError :: BundleWithoutReplacementUuid ) ;
100+ }
89101
90- // Calculate the total fee for the item.
102+ let item = SimItem :: try_from ( bundle ) ? ;
91103 let score = item. calculate_total_fee ( basefee) ;
92104
93- let mut inner = self . inner . write ( ) . unwrap ( ) ;
94-
105+ let mut inner = self . inner . write ( ) ;
95106 Self :: add_inner ( & mut inner, score, item, self . capacity ) ;
107+
108+ Ok ( ( ) )
96109 }
97110
98- /// Add an iterator of items to the cache. This locks the cache only once
99- pub fn add_items < I , Item > ( & self , item : I , basefee : u64 )
111+ /// Add an iterator of bundles to the cache. This locks the cache only once
112+ ///
113+ /// Bundles added should have a valid replacement UUID. Bundles without a replacement UUID will be skipped.
114+ pub fn add_bundles < I , Item > ( & self , item : I , basefee : u64 ) -> Result < ( ) , CacheError >
100115 where
101116 I : IntoIterator < Item = Item > ,
102- Item : Into < SimItem > ,
117+ Item : Into < SignetEthBundle > ,
103118 {
104- let iter = item. into_iter ( ) . map ( |item| {
119+ let mut inner = self . inner . write ( ) ;
120+
121+ for item in item. into_iter ( ) {
105122 let item = item. into ( ) ;
123+ let Ok ( item) = SimItem :: try_from ( item) else {
124+ // Skip invalid bundles
125+ continue ;
126+ } ;
106127 let score = item. calculate_total_fee ( basefee) ;
107- ( score, item)
108- } ) ;
128+ Self :: add_inner ( & mut inner, score, item, self . capacity ) ;
129+ }
130+
131+ Ok ( ( ) )
132+ }
109133
110- let mut inner = self . inner . write ( ) . unwrap ( ) ;
134+ /// Add a transaction to the cache.
135+ pub fn add_tx ( & self , tx : TxEnvelope , basefee : u64 ) {
136+ let item = SimItem :: from ( tx) ;
137+ let score = item. calculate_total_fee ( basefee) ;
138+
139+ let mut inner = self . inner . write ( ) ;
140+ Self :: add_inner ( & mut inner, score, item, self . capacity ) ;
141+ }
111142
112- for ( score, item) in iter {
143+ /// Add an iterator of transactions to the cache. This locks the cache only once
144+ pub fn add_txs < I > ( & self , item : I , basefee : u64 )
145+ where
146+ I : IntoIterator < Item = TxEnvelope > ,
147+ {
148+ let mut inner = self . inner . write ( ) ;
149+
150+ for item in item. into_iter ( ) {
151+ let item = SimItem :: from ( item) ;
152+ let score = item. calculate_total_fee ( basefee) ;
113153 Self :: add_inner ( & mut inner, score, item, self . capacity ) ;
114154 }
115155 }
116156
117157 /// Clean the cache by removing bundles that are not valid in the current
118158 /// block.
119159 pub fn clean ( & self , block_number : u64 , block_timestamp : u64 ) {
120- let mut inner = self . inner . write ( ) . unwrap ( ) ;
160+ let mut inner = self . inner . write ( ) ;
121161
122162 // Trim to capacity by dropping lower fees.
123- while inner. len ( ) > self . capacity {
124- inner. pop_first ( ) ;
163+ while inner. items . len ( ) > self . capacity {
164+ if let Some ( ( _, item) ) = inner. items . pop_first ( ) {
165+ // Drop the identifier from the seen cache as well.
166+ inner. seen . remove ( item. identifier ( ) . as_bytes ( ) ) ;
167+ }
125168 }
126169
127- inner. retain ( |_, value| {
128- let SimItem :: Bundle ( bundle) = value else {
129- return true ;
130- } ;
131- if bundle. bundle . block_number != block_number {
132- return false ;
133- }
134- if let Some ( timestamp) = bundle. min_timestamp ( ) {
135- if timestamp > block_timestamp {
136- return false ;
137- }
138- }
139- if let Some ( timestamp) = bundle. max_timestamp ( ) {
140- if timestamp < block_timestamp {
141- return false ;
170+ let CacheInner { ref mut items, ref mut seen } = * inner;
171+
172+ items. retain ( |_, item| {
173+ // Retain only items that are not bundles or are valid in the current block.
174+ if let SimItem :: Bundle ( bundle) = item {
175+ let should_remove = bundle. bundle . block_number == block_number
176+ && bundle. min_timestamp ( ) . is_some_and ( |ts| ts <= block_timestamp)
177+ && bundle. max_timestamp ( ) . is_some_and ( |ts| ts >= block_timestamp) ;
178+
179+ let retain = !should_remove;
180+
181+ if should_remove {
182+ seen. remove ( item. identifier ( ) . as_bytes ( ) ) ;
142183 }
184+ retain
185+ } else {
186+ true // Non-bundle items are retained
143187 }
144- true
145- } )
188+ } ) ;
146189 }
147190
148191 /// Clear the cache.
149192 pub fn clear ( & self ) {
150- let mut inner = self . inner . write ( ) . unwrap ( ) ;
151- inner. clear ( ) ;
193+ let mut inner = self . inner . write ( ) ;
194+ inner. items . clear ( ) ;
195+ inner. seen . clear ( ) ;
196+ }
197+ }
198+
199+ /// Internal cache data, meant to be protected by a lock.
200+ struct CacheInner {
201+ items : BTreeMap < u128 , SimItem > ,
202+ seen : HashSet < SimIdentifier < ' static > > ,
203+ }
204+
205+ impl fmt:: Debug for CacheInner {
206+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
207+ f. debug_struct ( "CacheInner" ) . finish ( )
208+ }
209+ }
210+
211+ impl CacheInner {
212+ fn new ( ) -> Self {
213+ Self { items : BTreeMap :: new ( ) , seen : HashSet :: new ( ) }
152214 }
153215}
154216
155217#[ cfg( test) ]
156218mod test {
219+ use alloy:: primitives:: b256;
220+
157221 use super :: * ;
158- use crate :: SimItem ;
159222
160223 #[ test]
161224 fn test_cache ( ) {
162225 let items = vec ! [
163- SimItem :: invalid_item_with_score ( 100 , 1 ) ,
164- SimItem :: invalid_item_with_score ( 100 , 2 ) ,
165- SimItem :: invalid_item_with_score ( 100 , 3 ) ,
226+ invalid_tx_with_score ( 100 , 1 ) ,
227+ invalid_tx_with_score ( 100 , 2 ) ,
228+ invalid_tx_with_score ( 100 , 3 ) ,
166229 ] ;
167230
168231 let cache = SimCache :: with_capacity ( 2 ) ;
169- cache. add_items ( items, 0 ) ;
232+ cache. add_txs ( items. clone ( ) , 0 ) ;
170233
171234 assert_eq ! ( cache. len( ) , 2 ) ;
172- assert_eq ! ( cache. get( 300 ) , Some ( SimItem :: invalid_item_with_score ( 100 , 3 ) ) ) ;
173- assert_eq ! ( cache. get( 200 ) , Some ( SimItem :: invalid_item_with_score ( 100 , 2 ) ) ) ;
235+ assert_eq ! ( cache. get( 300 ) , Some ( items [ 2 ] . clone ( ) . into ( ) ) ) ;
236+ assert_eq ! ( cache. get( 200 ) , Some ( items [ 1 ] . clone ( ) . into ( ) ) ) ;
174237 assert_eq ! ( cache. get( 100 ) , None ) ;
175238 }
176239
177240 #[ test]
178241 fn overlap_at_zero ( ) {
179242 let items = vec ! [
180- SimItem :: invalid_item_with_score( 1 , 1 ) ,
181- SimItem :: invalid_item_with_score( 1 , 1 ) ,
182- SimItem :: invalid_item_with_score( 1 , 1 ) ,
243+ invalid_tx_with_score_and_hash(
244+ 1 ,
245+ 1 ,
246+ b256!( "0xb36a5a0066980e8477d5d5cebf023728d3cfb837c719dc7f3aadb73d1a39f11f" ) ,
247+ ) ,
248+ invalid_tx_with_score_and_hash(
249+ 1 ,
250+ 1 ,
251+ b256!( "0x04d3629f341cdcc5f72969af3c7638e106b4b5620594e6831d86f03ea048e68a" ) ,
252+ ) ,
253+ invalid_tx_with_score_and_hash(
254+ 1 ,
255+ 1 ,
256+ b256!( "0x0f0b6a85c1ef6811bf86e92a3efc09f61feb1deca9da671119aaca040021598a" ) ,
257+ ) ,
183258 ] ;
184259
185260 let cache = SimCache :: with_capacity ( 2 ) ;
186- cache. add_items ( items, 0 ) ;
261+ cache. add_txs ( items. clone ( ) , 0 ) ;
187262
188- dbg ! ( & * cache. inner. read( ) . unwrap ( ) ) ;
263+ dbg ! ( & * cache. inner. read( ) ) ;
189264
190265 assert_eq ! ( cache. len( ) , 2 ) ;
191- assert_eq ! ( cache. get( 0 ) , Some ( SimItem :: invalid_item_with_score ( 1 , 1 ) ) ) ;
192- assert_eq ! ( cache. get( 1 ) , Some ( SimItem :: invalid_item_with_score ( 1 , 1 ) ) ) ;
266+ assert_eq ! ( cache. get( 0 ) , Some ( items [ 2 ] . clone ( ) . into ( ) ) ) ;
267+ assert_eq ! ( cache. get( 1 ) , Some ( items [ 0 ] . clone ( ) . into ( ) ) ) ;
193268 assert_eq ! ( cache. get( 2 ) , None ) ;
194269 }
270+
271+ fn invalid_tx_with_score ( gas_limit : u64 , mpfpg : u128 ) -> alloy:: consensus:: TxEnvelope {
272+ let tx = build_alloy_tx ( gas_limit, mpfpg) ;
273+
274+ TxEnvelope :: Eip1559 ( alloy:: consensus:: Signed :: new_unhashed (
275+ tx,
276+ alloy:: signers:: Signature :: test_signature ( ) ,
277+ ) )
278+ }
279+
280+ fn invalid_tx_with_score_and_hash (
281+ gas_limit : u64 ,
282+ mpfpg : u128 ,
283+ hash : alloy:: primitives:: B256 ,
284+ ) -> alloy:: consensus:: TxEnvelope {
285+ let tx = build_alloy_tx ( gas_limit, mpfpg) ;
286+
287+ TxEnvelope :: Eip1559 ( alloy:: consensus:: Signed :: new_unchecked (
288+ tx,
289+ alloy:: signers:: Signature :: test_signature ( ) ,
290+ hash,
291+ ) )
292+ }
293+
294+ fn build_alloy_tx ( gas_limit : u64 , mpfpg : u128 ) -> alloy:: consensus:: TxEip1559 {
295+ alloy:: consensus:: TxEip1559 {
296+ gas_limit,
297+ max_priority_fee_per_gas : mpfpg,
298+ max_fee_per_gas : alloy:: consensus:: constants:: GWEI_TO_WEI as u128 ,
299+ ..Default :: default ( )
300+ }
301+ }
195302}
0 commit comments