use crate::cache::{CacheError, SimIdentifier, SimItem, SimItemValidity, StateSource};
use alloy::consensus::{transaction::Recovered, TxEnvelope};
use core::fmt;
use lru::LruCache;
use parking_lot::RwLock;
use signet_bundle::{RecoveredBundle, SignetEthBundle};
use std::{
collections::{BTreeMap, HashSet},
mem::MaybeUninit,
num::NonZeroUsize,
ops::Deref,
sync::Arc,
};
use tracing::{instrument, Span};
#[derive(Clone)]
pub struct SimCache {
inner: Arc<RwLock<CacheStore>>,
capacity: usize,
}
impl fmt::Debug for SimCache {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SimCache").finish()
}
}
impl Default for SimCache {
fn default() -> Self {
Self::new()
}
}
impl SimCache {
pub fn new() -> Self {
Self { inner: Arc::new(RwLock::new(CacheStore::new())), capacity: 100 }
}
pub fn with_capacity(capacity: usize) -> Self {
Self { inner: Arc::new(RwLock::new(CacheStore::new())), capacity }
}
pub fn write_best_to(&self, buf: &mut [MaybeUninit<(u128, SimItem)>]) -> usize {
let cache = self.inner.read();
cache.items.iter().rev().zip(buf.iter_mut()).for_each(|((cache_rank, item), slot)| {
slot.write((*cache_rank, item.clone()));
});
std::cmp::min(cache.items.len(), buf.len())
}
pub fn read_best(&self, n: usize) -> Vec<(u128, SimItem)> {
let mut vec = Vec::with_capacity(n);
let n = self.write_best_to(vec.spare_capacity_mut());
unsafe { vec.set_len(n) };
vec
}
#[instrument(
level = "debug",
skip_all,
fields(
candidates_total = tracing::field::Empty,
candidates_checked = tracing::field::Empty,
valid_count = tracing::field::Empty,
future_count = tracing::field::Empty,
never_count = tracing::field::Empty,
)
)]
pub async fn read_best_valid<S, S2>(
&self,
n: usize,
source: &S,
host_source: &S2,
) -> Result<Vec<(u128, SimItem)>, Box<dyn std::error::Error>>
where
S: StateSource,
S2: StateSource,
{
let candidates: Vec<(u128, SimItem)> = {
let cache = self.inner.read();
cache.items.iter().rev().map(|(rank, item)| (*rank, item.clone())).collect()
};
let span = Span::current();
span.record("candidates_total", candidates.len());
let mut valid = Vec::with_capacity(n);
let mut never = Vec::new();
let mut future_count: u32 = 0;
let mut checked: u32 = 0;
for (rank, item) in &candidates {
if valid.len() >= n {
break;
}
checked += 1;
let validity = item.check(source, host_source).await?;
match validity {
SimItemValidity::Now => valid.push((*rank, item.clone())),
SimItemValidity::Never => never.push(*rank),
SimItemValidity::Future => future_count += 1,
}
}
span.record("candidates_checked", checked);
span.record("valid_count", valid.len());
span.record("future_count", future_count);
span.record("never_count", never.len());
if !never.is_empty() {
let mut cache = self.inner.write();
for rank in never {
cache.remove_and_disallow(rank);
}
}
Ok(valid)
}
pub fn len(&self) -> usize {
self.inner.read().items.len()
}
pub fn is_empty(&self) -> bool {
self.inner.read().items.is_empty()
}
pub fn get(&self, cache_rank: u128) -> Option<SimItem> {
self.inner.read().items.get(&cache_rank).cloned()
}
pub fn remove(&self, cache_rank: u128) -> Option<SimItem> {
let mut inner = self.inner.write();
inner.remove(cache_rank)
}
pub fn remove_and_disallow(&self, cache_rank: u128) -> Option<SimItem> {
let mut inner = self.inner.write();
inner.remove_and_disallow(cache_rank)
}
pub fn add_bundle(&self, bundle: SignetEthBundle, basefee: u64) -> Result<(), CacheError> {
if bundle.replacement_uuid().is_none() {
return Err(CacheError::BundleWithoutReplacementUuid);
}
let item = SimItem::try_from(bundle)?;
let cache_rank = item.calculate_total_fee(basefee);
let mut inner = self.inner.write();
inner.add_inner(cache_rank, item, self.capacity);
Ok(())
}
pub fn add_bundles<I, Item>(&self, item: I, basefee: u64)
where
I: IntoIterator<Item = Item>,
Item: Into<RecoveredBundle>,
{
let mut inner = self.inner.write();
inner.add_bundles(item, basefee, self.capacity);
}
pub fn add_tx(&self, tx: Recovered<TxEnvelope>, basefee: u64) {
let item = SimItem::from(tx);
let cache_rank = item.calculate_total_fee(basefee);
let mut inner = self.inner.write();
inner.add_inner(cache_rank, item, self.capacity);
}
pub fn add_txs<I>(&self, item: I, basefee: u64)
where
I: IntoIterator<Item = Recovered<TxEnvelope>>,
{
let mut inner = self.inner.write();
inner.add_txs(item, basefee, self.capacity);
}
pub fn clean(&self, block_number: u64, block_timestamp: u64) {
let mut inner = self.inner.write();
inner.clean(self.capacity, block_number, block_timestamp);
}
pub fn clear(&self) {
let mut inner = self.inner.write();
inner.clear();
}
}
struct CacheStore {
items: BTreeMap<u128, SimItem>,
seen: HashSet<SimIdentifier<'static>>,
disallowed: LruCache<SimIdentifier<'static>, ()>,
}
impl fmt::Debug for CacheStore {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CacheInner").finish()
}
}
impl CacheStore {
fn new() -> Self {
Self {
items: BTreeMap::new(),
seen: HashSet::new(),
disallowed: LruCache::new(NonZeroUsize::new(128).unwrap()),
}
}
fn add_inner(&mut self, mut cache_rank: u128, item: SimItem, capacity: usize) {
if self.disallowed.contains(&item.identifier_owned()) {
return;
}
if !self.seen.insert(item.identifier_owned()) {
return;
}
while self.items.contains_key(&cache_rank) && cache_rank != 0 {
cache_rank = cache_rank.saturating_sub(1);
}
if self.items.len() >= capacity {
if let Some((_, item)) = self.items.pop_first() {
self.seen.remove(&item.identifier_owned());
}
}
self.items.insert(cache_rank, item.clone());
}
fn add_bundles<I, T>(&mut self, item: I, basefee: u64, capacity: usize)
where
I: IntoIterator<Item = T>,
T: Into<RecoveredBundle>,
{
for item in item.into_iter() {
let item = item.into();
let Ok(item) = SimItem::try_from(item) else {
continue;
};
let cache_rank = item.calculate_total_fee(basefee);
self.add_inner(cache_rank, item, capacity);
}
}
fn add_txs<I>(&mut self, item: I, basefee: u64, capacity: usize)
where
I: IntoIterator<Item = Recovered<TxEnvelope>>,
{
for item in item.into_iter() {
let item = SimItem::from(item);
let cache_rank = item.calculate_total_fee(basefee);
self.add_inner(cache_rank, item, capacity);
}
}
fn remove(&mut self, cache_rank: u128) -> Option<SimItem> {
if let Some(item) = self.items.remove(&cache_rank) {
self.seen.remove(item.identifier().as_bytes());
Some(item)
} else {
None
}
}
fn remove_and_disallow(&mut self, cache_rank: u128) -> Option<SimItem> {
self.remove(cache_rank).inspect(|item| {
self.disallowed.put(item.identifier_owned(), ());
})
}
fn clean(&mut self, capacity: usize, block_number: u64, block_timestamp: u64) {
while self.items.len() > capacity {
if let Some(key) = self.items.keys().next() {
self.remove_and_disallow(*key);
}
}
self.items.retain(|_, item| {
if let SimItem::Bundle(bundle) = item.deref() {
let ts_range = bundle.valid_timestamp_range();
let bundle_block = bundle.block_number();
let now = block_number == bundle_block && ts_range.contains(&block_timestamp);
let never =
!now && (block_number > bundle_block || block_timestamp > *ts_range.end());
if !now {
self.seen.remove(item.identifier().as_bytes());
}
if never {
self.disallowed.put(item.identifier_owned(), ());
}
now
} else {
true }
});
}
fn clear(&mut self) {
self.items.clear();
self.seen.clear();
}
}
#[cfg(test)]
mod test {
use alloy::primitives::{b256, Address};
use super::*;
#[test]
fn test_cache() {
let items = vec![
invalid_tx_with_score(100, 1),
invalid_tx_with_score(100, 2),
invalid_tx_with_score(100, 3),
];
let cache = SimCache::with_capacity(2);
cache.add_txs(items.clone(), 0);
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(300), Some(items[2].clone().into()));
assert_eq!(cache.get(200), Some(items[1].clone().into()));
assert_eq!(cache.get(100), None);
}
#[test]
fn overlap_at_zero() {
let items = vec![
invalid_tx_with_score_and_hash(
1,
1,
b256!("0xb36a5a0066980e8477d5d5cebf023728d3cfb837c719dc7f3aadb73d1a39f11f"),
),
invalid_tx_with_score_and_hash(
1,
1,
b256!("0x04d3629f341cdcc5f72969af3c7638e106b4b5620594e6831d86f03ea048e68a"),
),
invalid_tx_with_score_and_hash(
1,
1,
b256!("0x0f0b6a85c1ef6811bf86e92a3efc09f61feb1deca9da671119aaca040021598a"),
),
];
let cache = SimCache::with_capacity(2);
cache.add_txs(items.clone(), 0);
dbg!(&*cache.inner.read());
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(0), Some(items[2].clone().into()));
assert_eq!(cache.get(1), Some(items[0].clone().into()));
assert_eq!(cache.get(2), None);
}
#[test]
fn test_cache_with_bundles() {
let items = vec![
invalid_bundle_with_score(100, 1, "fbcbb9ce-2bef-4587-9c5f-61f606ca0a1a".to_string()),
invalid_bundle_with_score(100, 2, "39637ce4-5f33-4eb6-8893-8cc325a6cca3".to_string()),
invalid_bundle_with_score(100, 3, "1c008717-b187-4e53-9601-25435f5fe8b7".to_string()),
];
let cache = SimCache::with_capacity(2);
cache.add_bundles(items.clone(), 0);
assert_eq!(cache.len(), 2);
assert_eq!(cache.get(300), Some(items[2].clone().try_into().unwrap()));
assert_eq!(cache.get(200), Some(items[1].clone().try_into().unwrap()));
assert_eq!(cache.get(100), None);
}
fn invalid_bundle_with_score(
gas_limit: u64,
mpfpg: u128,
replacement_uuid: String,
) -> signet_bundle::RecoveredBundle {
let tx = invalid_tx_with_score(gas_limit, mpfpg);
signet_bundle::RecoveredBundle::new_unchecked(
vec![tx],
vec![],
1,
Some(2),
Some(3),
vec![],
Some(replacement_uuid.clone()),
vec![],
None,
None,
vec![],
Default::default(),
)
}
fn invalid_tx_with_score(
gas_limit: u64,
mpfpg: u128,
) -> Recovered<alloy::consensus::TxEnvelope> {
let tx = build_alloy_tx(gas_limit, mpfpg);
Recovered::new_unchecked(
TxEnvelope::Eip1559(alloy::consensus::Signed::new_unhashed(
tx,
alloy::signers::Signature::test_signature(),
)),
Address::with_last_byte(7),
)
}
fn invalid_tx_with_score_and_hash(
gas_limit: u64,
mpfpg: u128,
hash: alloy::primitives::B256,
) -> Recovered<alloy::consensus::TxEnvelope> {
let tx = build_alloy_tx(gas_limit, mpfpg);
Recovered::new_unchecked(
TxEnvelope::Eip1559(alloy::consensus::Signed::new_unchecked(
tx,
alloy::signers::Signature::test_signature(),
hash,
)),
Address::with_last_byte(8),
)
}
fn build_alloy_tx(gas_limit: u64, mpfpg: u128) -> alloy::consensus::TxEip1559 {
alloy::consensus::TxEip1559 {
gas_limit,
max_priority_fee_per_gas: mpfpg,
max_fee_per_gas: alloy::consensus::constants::GWEI_TO_WEI as u128,
..Default::default()
}
}
}