1use crate::cache::{CacheError, SimIdentifier, SimItem, SimItemValidity, StateSource};
2use alloy::consensus::{transaction::Recovered, TxEnvelope};
3use core::fmt;
4use lru::LruCache;
5use parking_lot::RwLock;
6use signet_bundle::{RecoveredBundle, SignetEthBundle};
7use std::{
8 collections::{BTreeMap, HashSet},
9 mem::MaybeUninit,
10 num::NonZeroUsize,
11 ops::Deref,
12 sync::Arc,
13};
14use tracing::{instrument, Span};
15
16#[derive(Clone)]
20pub struct SimCache {
21 inner: Arc<RwLock<CacheStore>>,
22 capacity: usize,
23}
24
25impl fmt::Debug for SimCache {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("SimCache").finish()
28 }
29}
30
31impl Default for SimCache {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl SimCache {
38 pub fn new() -> Self {
40 Self { inner: Arc::new(RwLock::new(CacheStore::new())), capacity: 100 }
41 }
42
43 pub fn with_capacity(capacity: usize) -> Self {
45 Self { inner: Arc::new(RwLock::new(CacheStore::new())), capacity }
46 }
47
48 pub fn write_best_to(&self, buf: &mut [MaybeUninit<(u128, SimItem)>]) -> usize {
50 let cache = self.inner.read();
51 cache.items.iter().rev().zip(buf.iter_mut()).for_each(|((cache_rank, item), slot)| {
52 slot.write((*cache_rank, item.clone()));
54 });
55 std::cmp::min(cache.items.len(), buf.len())
57 }
58
59 pub fn read_best(&self, n: usize) -> Vec<(u128, SimItem)> {
61 let mut vec = Vec::with_capacity(n);
62 let n = self.write_best_to(vec.spare_capacity_mut());
63 unsafe { vec.set_len(n) };
65 vec
66 }
67
68 #[instrument(
78 level = "debug",
79 skip_all,
80 fields(
81 candidates_total = tracing::field::Empty,
82 candidates_checked = tracing::field::Empty,
83 valid_count = tracing::field::Empty,
84 future_count = tracing::field::Empty,
85 never_count = tracing::field::Empty,
86 )
87 )]
88 pub async fn read_best_valid<S, S2>(
89 &self,
90 n: usize,
91 source: &S,
92 host_source: &S2,
93 ) -> Result<Vec<(u128, SimItem)>, Box<dyn std::error::Error>>
94 where
95 S: StateSource,
96 S2: StateSource,
97 {
98 let candidates: Vec<(u128, SimItem)> = {
101 let cache = self.inner.read();
102 cache.items.iter().rev().map(|(rank, item)| (*rank, item.clone())).collect()
104 };
105
106 let span = Span::current();
107 span.record("candidates_total", candidates.len());
108
109 let mut valid = Vec::with_capacity(n);
110 let mut never = Vec::new();
111 let mut future_count: u32 = 0;
112 let mut checked: u32 = 0;
113
114 for (rank, item) in &candidates {
115 if valid.len() >= n {
116 break;
117 }
118 checked += 1;
119
120 let validity = item.check(source, host_source).await?;
121
122 match validity {
123 SimItemValidity::Now => valid.push((*rank, item.clone())),
124 SimItemValidity::Never => never.push(*rank),
125 SimItemValidity::Future => future_count += 1,
126 }
127 }
128
129 span.record("candidates_checked", checked);
130 span.record("valid_count", valid.len());
131 span.record("future_count", future_count);
132 span.record("never_count", never.len());
133
134 if !never.is_empty() {
136 let mut cache = self.inner.write();
137 for rank in never {
138 cache.remove_and_disallow(rank);
139 }
140 }
141
142 Ok(valid)
143 }
144
145 pub fn len(&self) -> usize {
147 self.inner.read().items.len()
148 }
149
150 pub fn is_empty(&self) -> bool {
152 self.inner.read().items.is_empty()
153 }
154
155 pub fn get(&self, cache_rank: u128) -> Option<SimItem> {
157 self.inner.read().items.get(&cache_rank).cloned()
158 }
159
160 pub fn remove(&self, cache_rank: u128) -> Option<SimItem> {
162 let mut inner = self.inner.write();
163 inner.remove(cache_rank)
164 }
165
166 pub fn remove_and_disallow(&self, cache_rank: u128) -> Option<SimItem> {
168 let mut inner = self.inner.write();
169 inner.remove_and_disallow(cache_rank)
170 }
171
172 pub fn add_bundle(&self, bundle: SignetEthBundle, basefee: u64) -> Result<(), CacheError> {
174 if bundle.replacement_uuid().is_none() {
175 return Err(CacheError::BundleWithoutReplacementUuid);
177 }
178
179 let item = SimItem::try_from(bundle)?;
180 let cache_rank = item.calculate_total_fee(basefee);
181
182 let mut inner = self.inner.write();
183 inner.add_inner(cache_rank, item, self.capacity);
184
185 Ok(())
186 }
187
188 pub fn add_bundles<I, Item>(&self, item: I, basefee: u64)
192 where
193 I: IntoIterator<Item = Item>,
194 Item: Into<RecoveredBundle>,
195 {
196 let mut inner = self.inner.write();
197 inner.add_bundles(item, basefee, self.capacity);
198 }
199
200 pub fn add_tx(&self, tx: Recovered<TxEnvelope>, basefee: u64) {
202 let item = SimItem::from(tx);
203 let cache_rank = item.calculate_total_fee(basefee);
204
205 let mut inner = self.inner.write();
206 inner.add_inner(cache_rank, item, self.capacity);
207 }
208
209 pub fn add_txs<I>(&self, item: I, basefee: u64)
211 where
212 I: IntoIterator<Item = Recovered<TxEnvelope>>,
213 {
214 let mut inner = self.inner.write();
215 inner.add_txs(item, basefee, self.capacity);
216 }
217
218 pub fn clean(&self, block_number: u64, block_timestamp: u64) {
221 let mut inner = self.inner.write();
222 inner.clean(self.capacity, block_number, block_timestamp);
223 }
224
225 pub fn clear(&self) {
227 let mut inner = self.inner.write();
228 inner.clear();
229 }
230}
231
232struct CacheStore {
234 items: BTreeMap<u128, SimItem>,
236
237 seen: HashSet<SimIdentifier<'static>>,
240
241 disallowed: LruCache<SimIdentifier<'static>, ()>,
244}
245
246impl fmt::Debug for CacheStore {
247 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
248 f.debug_struct("CacheInner").finish()
249 }
250}
251
252impl CacheStore {
253 fn new() -> Self {
254 Self {
255 items: BTreeMap::new(),
256 seen: HashSet::new(),
257 disallowed: LruCache::new(NonZeroUsize::new(128).unwrap()),
258 }
259 }
260
261 fn add_inner(&mut self, mut cache_rank: u128, item: SimItem, capacity: usize) {
263 if self.disallowed.contains(&item.identifier_owned()) {
265 return;
266 }
267
268 if !self.seen.insert(item.identifier_owned()) {
270 return;
271 }
272
273 while self.items.contains_key(&cache_rank) && cache_rank != 0 {
275 cache_rank = cache_rank.saturating_sub(1);
276 }
277
278 if self.items.len() >= capacity {
279 if let Some((_, item)) = self.items.pop_first() {
281 self.seen.remove(&item.identifier_owned());
282 }
283 }
284
285 self.items.insert(cache_rank, item.clone());
286 }
287
288 fn add_bundles<I, T>(&mut self, item: I, basefee: u64, capacity: usize)
289 where
290 I: IntoIterator<Item = T>,
291 T: Into<RecoveredBundle>,
292 {
293 for item in item.into_iter() {
294 let item = item.into();
295 let Ok(item) = SimItem::try_from(item) else {
296 continue;
298 };
299 let cache_rank = item.calculate_total_fee(basefee);
300 self.add_inner(cache_rank, item, capacity);
301 }
302 }
303
304 fn add_txs<I>(&mut self, item: I, basefee: u64, capacity: usize)
305 where
306 I: IntoIterator<Item = Recovered<TxEnvelope>>,
307 {
308 for item in item.into_iter() {
309 let item = SimItem::from(item);
310 let cache_rank = item.calculate_total_fee(basefee);
311 self.add_inner(cache_rank, item, capacity);
312 }
313 }
314
315 fn remove(&mut self, cache_rank: u128) -> Option<SimItem> {
317 if let Some(item) = self.items.remove(&cache_rank) {
318 self.seen.remove(item.identifier().as_bytes());
319 Some(item)
320 } else {
321 None
322 }
323 }
324 fn remove_and_disallow(&mut self, cache_rank: u128) -> Option<SimItem> {
327 self.remove(cache_rank).inspect(|item| {
328 self.disallowed.put(item.identifier_owned(), ());
329 })
330 }
331
332 fn clean(&mut self, capacity: usize, block_number: u64, block_timestamp: u64) {
335 while self.items.len() > capacity {
337 if let Some(key) = self.items.keys().next() {
338 self.remove_and_disallow(*key);
339 }
340 }
341
342 self.items.retain(|_, item| {
343 if let SimItem::Bundle(bundle) = item.deref() {
345 let ts_range = bundle.valid_timestamp_range();
346 let bundle_block = bundle.block_number();
347
348 let now = block_number == bundle_block && ts_range.contains(&block_timestamp);
351
352 let never =
355 !now && (block_number > bundle_block || block_timestamp > *ts_range.end());
356
357 if !now {
358 self.seen.remove(item.identifier().as_bytes());
359 }
360
361 if never {
362 self.disallowed.put(item.identifier_owned(), ());
363 }
364
365 now
366 } else {
367 true }
369 });
370 }
371
372 fn clear(&mut self) {
373 self.items.clear();
374 self.seen.clear();
375 }
376}
377
378#[cfg(test)]
379mod test {
380
381 use alloy::primitives::{b256, Address};
382
383 use super::*;
384
385 #[test]
386 fn test_cache() {
387 let items = vec![
388 invalid_tx_with_score(100, 1),
389 invalid_tx_with_score(100, 2),
390 invalid_tx_with_score(100, 3),
391 ];
392
393 let cache = SimCache::with_capacity(2);
394 cache.add_txs(items.clone(), 0);
395
396 assert_eq!(cache.len(), 2);
397 assert_eq!(cache.get(300), Some(items[2].clone().into()));
398 assert_eq!(cache.get(200), Some(items[1].clone().into()));
399 assert_eq!(cache.get(100), None);
400 }
401
402 #[test]
403 fn overlap_at_zero() {
404 let items = vec![
405 invalid_tx_with_score_and_hash(
406 1,
407 1,
408 b256!("0xb36a5a0066980e8477d5d5cebf023728d3cfb837c719dc7f3aadb73d1a39f11f"),
409 ),
410 invalid_tx_with_score_and_hash(
411 1,
412 1,
413 b256!("0x04d3629f341cdcc5f72969af3c7638e106b4b5620594e6831d86f03ea048e68a"),
414 ),
415 invalid_tx_with_score_and_hash(
416 1,
417 1,
418 b256!("0x0f0b6a85c1ef6811bf86e92a3efc09f61feb1deca9da671119aaca040021598a"),
419 ),
420 ];
421
422 let cache = SimCache::with_capacity(2);
423 cache.add_txs(items.clone(), 0);
424
425 dbg!(&*cache.inner.read());
426
427 assert_eq!(cache.len(), 2);
428 assert_eq!(cache.get(0), Some(items[2].clone().into()));
429 assert_eq!(cache.get(1), Some(items[0].clone().into()));
430 assert_eq!(cache.get(2), None);
431 }
432
433 #[test]
434 fn test_cache_with_bundles() {
435 let items = vec![
436 invalid_bundle_with_score(100, 1, "fbcbb9ce-2bef-4587-9c5f-61f606ca0a1a".to_string()),
437 invalid_bundle_with_score(100, 2, "39637ce4-5f33-4eb6-8893-8cc325a6cca3".to_string()),
438 invalid_bundle_with_score(100, 3, "1c008717-b187-4e53-9601-25435f5fe8b7".to_string()),
439 ];
440
441 let cache = SimCache::with_capacity(2);
442
443 cache.add_bundles(items.clone(), 0);
444
445 assert_eq!(cache.len(), 2);
446 assert_eq!(cache.get(300), Some(items[2].clone().try_into().unwrap()));
447 assert_eq!(cache.get(200), Some(items[1].clone().try_into().unwrap()));
448 assert_eq!(cache.get(100), None);
449 }
450
451 fn invalid_bundle_with_score(
452 gas_limit: u64,
453 mpfpg: u128,
454 replacement_uuid: String,
455 ) -> signet_bundle::RecoveredBundle {
456 let tx = invalid_tx_with_score(gas_limit, mpfpg);
457 signet_bundle::RecoveredBundle::new_unchecked(
458 vec![tx],
459 vec![],
460 1,
461 Some(2),
462 Some(3),
463 vec![],
464 Some(replacement_uuid.clone()),
465 vec![],
466 None,
467 None,
468 vec![],
469 Default::default(),
470 )
471 }
472
473 fn invalid_tx_with_score(
474 gas_limit: u64,
475 mpfpg: u128,
476 ) -> Recovered<alloy::consensus::TxEnvelope> {
477 let tx = build_alloy_tx(gas_limit, mpfpg);
478
479 Recovered::new_unchecked(
480 TxEnvelope::Eip1559(alloy::consensus::Signed::new_unhashed(
481 tx,
482 alloy::signers::Signature::test_signature(),
483 )),
484 Address::with_last_byte(7),
485 )
486 }
487
488 fn invalid_tx_with_score_and_hash(
489 gas_limit: u64,
490 mpfpg: u128,
491 hash: alloy::primitives::B256,
492 ) -> Recovered<alloy::consensus::TxEnvelope> {
493 let tx = build_alloy_tx(gas_limit, mpfpg);
494
495 Recovered::new_unchecked(
496 TxEnvelope::Eip1559(alloy::consensus::Signed::new_unchecked(
497 tx,
498 alloy::signers::Signature::test_signature(),
499 hash,
500 )),
501 Address::with_last_byte(8),
502 )
503 }
504
505 fn build_alloy_tx(gas_limit: u64, mpfpg: u128) -> alloy::consensus::TxEip1559 {
506 alloy::consensus::TxEip1559 {
507 gas_limit,
508 max_priority_fee_per_gas: mpfpg,
509 max_fee_per_gas: alloy::consensus::constants::GWEI_TO_WEI as u128,
510 ..Default::default()
511 }
512 }
513}