1
- use crate :: SimItem ;
1
+ use crate :: { item:: SimIdentifier , CacheError , SimItem } ;
2
+ use alloy:: consensus:: TxEnvelope ;
2
3
use core:: fmt;
4
+ use parking_lot:: RwLock ;
5
+ use signet_bundle:: SignetEthBundle ;
3
6
use std:: {
4
- collections:: BTreeMap ,
5
- sync:: { Arc , RwLock , RwLockWriteGuard } ,
7
+ collections:: { BTreeMap , HashSet } ,
8
+ sync:: Arc ,
6
9
} ;
7
10
8
11
/// A cache for the simulator.
9
12
///
10
13
/// This cache is used to store the items that are being simulated.
11
14
#[ derive( Clone ) ]
12
15
pub struct SimCache {
13
- inner : Arc < RwLock < BTreeMap < u128 , SimItem > > > ,
16
+ inner : Arc < RwLock < CacheInner > > ,
14
17
capacity : usize ,
15
18
}
16
19
@@ -27,169 +30,273 @@ impl Default for SimCache {
27
30
}
28
31
29
32
impl SimCache {
30
- /// Create a new `SimCache` instance.
33
+ /// Create a new `SimCache` instance, with a default capacity of `100` .
31
34
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 }
33
36
}
34
37
35
38
/// Create a new `SimCache` instance with a given capacity.
36
39
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 }
38
41
}
39
42
40
43
/// Get an iterator over the best items in the cache.
41
44
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 ( )
43
46
}
44
47
45
48
/// Get the number of items in the cache.
46
49
pub fn len ( & self ) -> usize {
47
- self . inner . read ( ) . unwrap ( ) . len ( )
50
+ self . inner . read ( ) . items . len ( )
48
51
}
49
52
50
53
/// True if the cache is empty.
51
54
pub fn is_empty ( & self ) -> bool {
52
- self . inner . read ( ) . unwrap ( ) . is_empty ( )
55
+ self . inner . read ( ) . items . is_empty ( )
53
56
}
54
57
55
58
/// Get an item by key.
56
59
pub fn get ( & self , key : u128 ) -> Option < SimItem > {
57
- self . inner . read ( ) . unwrap ( ) . get ( & key) . cloned ( )
60
+ self . inner . read ( ) . items . get ( & key) . cloned ( )
58
61
}
59
62
60
63
/// Remove an item by key.
61
64
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
+ }
63
72
}
64
73
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
+
71
80
// 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 {
73
82
score = score. saturating_sub ( 1 ) ;
74
83
}
75
84
76
- if guard . len ( ) >= capacity {
85
+ if inner . items . len ( ) >= capacity {
77
86
// 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
+ }
79
90
}
80
91
81
- guard . entry ( score) . or_insert ( item ) ;
92
+ inner . items . insert ( score, item . clone ( ) ) ;
82
93
}
83
94
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
+ }
89
101
90
- // Calculate the total fee for the item.
102
+ let item = SimItem :: try_from ( bundle ) ? ;
91
103
let score = item. calculate_total_fee ( basefee) ;
92
104
93
- let mut inner = self . inner . write ( ) . unwrap ( ) ;
94
-
105
+ let mut inner = self . inner . write ( ) ;
95
106
Self :: add_inner ( & mut inner, score, item, self . capacity ) ;
107
+
108
+ Ok ( ( ) )
96
109
}
97
110
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 >
100
115
where
101
116
I : IntoIterator < Item = Item > ,
102
- Item : Into < SimItem > ,
117
+ Item : Into < SignetEthBundle > ,
103
118
{
104
- let iter = item. into_iter ( ) . map ( |item| {
119
+ let mut inner = self . inner . write ( ) ;
120
+
121
+ for item in item. into_iter ( ) {
105
122
let item = item. into ( ) ;
123
+ let Ok ( item) = SimItem :: try_from ( item) else {
124
+ // Skip invalid bundles
125
+ continue ;
126
+ } ;
106
127
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
+ }
109
133
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
+ }
111
142
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) ;
113
153
Self :: add_inner ( & mut inner, score, item, self . capacity ) ;
114
154
}
115
155
}
116
156
117
157
/// Clean the cache by removing bundles that are not valid in the current
118
158
/// block.
119
159
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 ( ) ;
121
161
122
162
// 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
+ }
125
168
}
126
169
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 ( ) ) ;
142
183
}
184
+ retain
185
+ } else {
186
+ true // Non-bundle items are retained
143
187
}
144
- true
145
- } )
188
+ } ) ;
146
189
}
147
190
148
191
/// Clear the cache.
149
192
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 ( ) }
152
214
}
153
215
}
154
216
155
217
#[ cfg( test) ]
156
218
mod test {
219
+ use alloy:: primitives:: b256;
220
+
157
221
use super :: * ;
158
- use crate :: SimItem ;
159
222
160
223
#[ test]
161
224
fn test_cache ( ) {
162
225
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 ) ,
166
229
] ;
167
230
168
231
let cache = SimCache :: with_capacity ( 2 ) ;
169
- cache. add_items ( items, 0 ) ;
232
+ cache. add_txs ( items. clone ( ) , 0 ) ;
170
233
171
234
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 ( ) ) ) ;
174
237
assert_eq ! ( cache. get( 100 ) , None ) ;
175
238
}
176
239
177
240
#[ test]
178
241
fn overlap_at_zero ( ) {
179
242
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
+ ) ,
183
258
] ;
184
259
185
260
let cache = SimCache :: with_capacity ( 2 ) ;
186
- cache. add_items ( items, 0 ) ;
261
+ cache. add_txs ( items. clone ( ) , 0 ) ;
187
262
188
- dbg ! ( & * cache. inner. read( ) . unwrap ( ) ) ;
263
+ dbg ! ( & * cache. inner. read( ) ) ;
189
264
190
265
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 ( ) ) ) ;
193
268
assert_eq ! ( cache. get( 2 ) , None ) ;
194
269
}
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
+ }
195
302
}
0 commit comments