Skip to content

Commit df58e95

Browse files
Evalirprestwichanna-carroll
authored
feat(sim): dedup sim cache items (#74)
* feat(sim): dedup sim cache items Uses an identifier to have a seen cache for items, keeping it up to date with the actual `SimItem` cache. * chore: fix tests * chore: clippy * chore: import b256 * chore: clippy * chore: simplify locking * chore: use parking_lot rwlock (its faster) * chore: debug impl * chore: memoize identifier on item directly * chore: fix tests * feat: new ingestion api * chore: rm dashmap * chore: misc reordering * chore: add lifetimes * feat: make retrieving identifiers more efficient * fix: manually impl hash and partial eq * log identifier * add identifier to logs * chore: remove redundant checks, document behavior * chore: fmt/clppy * chore: remove HER * chore: display impl on simitem --------- Co-authored-by: James <[email protected]> Co-authored-by: Anna Carroll <[email protected]>
1 parent 514bc7c commit df58e95

File tree

8 files changed

+307
-111
lines changed

8 files changed

+307
-111
lines changed

crates/sim/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ alloy.workspace = true
2020
tokio.workspace = true
2121
tracing.workspace = true
2222
trevm.workspace = true
23+
thiserror.workspace = true
24+
25+
parking_lot.workspace = true
2326

2427
[dev-dependencies]
2528
tracing-subscriber.workspace = true
29+
alloy = { workspace = true, features = ["getrandom"] }

crates/sim/src/cache.rs

Lines changed: 181 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
use crate::SimItem;
1+
use crate::{item::SimIdentifier, CacheError, SimItem};
2+
use alloy::consensus::TxEnvelope;
23
use core::fmt;
4+
use parking_lot::RwLock;
5+
use signet_bundle::SignetEthBundle;
36
use 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)]
1215
pub 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

2932
impl 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)]
156218
mod 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
}

crates/sim/src/env.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,7 @@ where
302302
let score = beneficiary_balance.saturating_sub(initial_beneficiary_balance);
303303

304304
trace!(
305+
?identifier,
305306
gas_used = gas_used,
306307
score = %score,
307308
reverted = !success,
@@ -343,6 +344,7 @@ where
343344
let cache = trevm.into_db().into_cache();
344345

345346
trace!(
347+
?identifier,
346348
gas_used = gas_used,
347349
score = %score,
348350
"Bundle simulation successful"

0 commit comments

Comments
 (0)