use alloy_chains::NamedChain;
use alloy_primitives::{Address, BlockNumber};
use crate::cache::block_range::{BlockRangeCache, Mergeable};
use crate::gas::calculator::GasCostResult;
impl Mergeable for GasCostResult {
fn merge(&mut self, other: &Self) {
self.total_gas_cost = self.total_gas_cost + other.total_gas_cost;
self.transaction_count += other.transaction_count;
}
}
#[derive(Debug, Clone, Default)]
pub struct GasCache {
inner: BlockRangeCache<(Address, Address), GasCostResult>,
}
impl GasCache {
pub fn get(
&self,
from: Address,
to: Address,
start_block: BlockNumber,
end_block: BlockNumber,
) -> Option<GasCostResult> {
self.inner.get(&(from, to), start_block, end_block)
}
pub fn insert(
&mut self,
from: Address,
to: Address,
start_block: BlockNumber,
end_block: BlockNumber,
result: GasCostResult,
) {
self.inner
.insert((from, to), start_block, end_block, result);
}
pub fn calculate_gaps(
&self,
chain: NamedChain,
from: Address,
to: Address,
start_block: BlockNumber,
end_block: BlockNumber,
) -> (Option<GasCostResult>, Vec<(BlockNumber, BlockNumber)>) {
self.inner
.calculate_gaps(&(from, to), start_block, end_block, || {
GasCostResult::new(chain, from, to)
})
}
pub fn clear_signer_data(&mut self, from: Address, to: Address) {
self.inner
.retain(|(cached_from, cached_to), _, _| *cached_from != from || *cached_to != to);
}
pub fn clear_old_blocks(&mut self, min_block: BlockNumber) {
self.inner.retain(|_, _, end_block| end_block >= min_block);
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{TransactionCount, WeiAmount};
use alloy_chains::NamedChain;
use alloy_primitives::Address;
fn create_test_result(
chain: NamedChain,
from: Address,
to: Address,
tx_count: usize,
gas_cost: u64,
) -> GasCostResult {
let mut result = GasCostResult::new(chain, from, to);
result.transaction_count = TransactionCount::new(tx_count);
result.total_gas_cost = WeiAmount::from(gas_cost);
result
}
#[test]
fn test_cache_insert_and_get() {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let result = create_test_result(NamedChain::Mainnet, from, to, 5, 100_000);
cache.insert(from, to, 100, 200, result.clone());
let cached = cache.get(from, to, 100, 200);
assert!(cached.is_some());
assert_eq!(cached.unwrap().transaction_count, TransactionCount::new(5));
let cached = cache.get(from, to, 120, 180);
assert!(cached.is_some());
let cached = cache.get(from, to, 50, 300);
assert!(cached.is_none());
}
#[test]
fn test_calculate_gaps() {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
cache.insert(
from,
to,
100,
200,
create_test_result(NamedChain::Mainnet, from, to, 5, 100_000),
);
cache.insert(
from,
to,
300,
400,
create_test_result(NamedChain::Mainnet, from, to, 3, 60_000),
);
cache.insert(
from,
to,
600,
700,
create_test_result(NamedChain::Mainnet, from, to, 2, 40_000),
);
let (result, gaps) = cache.calculate_gaps(NamedChain::Mainnet, from, to, 50, 800);
assert!(result.is_some());
assert_eq!(gaps.len(), 4);
assert_eq!(gaps[0], (50, 99));
assert_eq!(gaps[1], (201, 299));
assert_eq!(gaps[2], (401, 599));
assert_eq!(gaps[3], (701, 800));
assert_eq!(result.unwrap().transaction_count, TransactionCount::new(10));
}
#[test]
fn test_overlap_merging() {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
cache.insert(
from,
to,
100,
300,
create_test_result(NamedChain::Mainnet, from, to, 5, 100_000),
);
cache.insert(
from,
to,
250,
400,
create_test_result(NamedChain::Mainnet, from, to, 3, 60_000),
);
assert_eq!(cache.len(), 1);
let cached = cache.get(from, to, 100, 400);
assert!(cached.is_some());
let result = cached.unwrap();
assert_eq!(result.transaction_count, TransactionCount::new(8));
assert_eq!(result.total_gas_cost, WeiAmount::from(160_000u64));
}
mod proptests {
use super::*;
use proptest::prelude::*;
fn block_range_strategy() -> impl Strategy<Value = (BlockNumber, BlockNumber)> {
(0u64..100_000u64)
.prop_flat_map(|start| (Just(start), start..start.saturating_add(10_000)))
}
fn cached_ranges_strategy() -> impl Strategy<Value = Vec<(BlockNumber, BlockNumber)>> {
prop::collection::vec(block_range_strategy(), 0..10).prop_map(|mut ranges| {
ranges.sort_by_key(|(start, _)| *start);
let mut non_overlapping = Vec::new();
let mut last_end = 0u64;
for (start, end) in ranges {
let adjusted_start = start.max(last_end + 2);
if adjusted_start < end {
non_overlapping.push((adjusted_start, end));
last_end = end;
}
}
non_overlapping
})
}
proptest! {
#[test]
fn test_gaps_never_overlap_with_cached(
cached_ranges in cached_ranges_strategy(),
(query_start, query_end) in block_range_strategy()
) {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let chain = NamedChain::Mainnet;
for (start, end) in &cached_ranges {
cache.insert(from, to, *start, *end, create_test_result(chain, from, to, 1, 1000));
}
let (_, gaps) = cache.calculate_gaps(chain, from, to, query_start, query_end);
for (gap_start, gap_end) in &gaps {
for (cached_start, cached_end) in &cached_ranges {
if *cached_end < query_start || *cached_start > query_end {
continue;
}
let no_overlap = *gap_end < *cached_start || *gap_start > *cached_end;
prop_assert!(
no_overlap,
"Gap [{gap_start}, {gap_end}] overlaps with cached range [{cached_start}, {cached_end}]"
);
}
}
}
#[test]
fn test_gaps_are_sorted(
cached_ranges in cached_ranges_strategy(),
(query_start, query_end) in block_range_strategy()
) {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let chain = NamedChain::Mainnet;
for (start, end) in &cached_ranges {
cache.insert(from, to, *start, *end, create_test_result(chain, from, to, 1, 1000));
}
let (_, gaps) = cache.calculate_gaps(chain, from, to, query_start, query_end);
for i in 1..gaps.len() {
prop_assert!(
gaps[i - 1].0 < gaps[i].0,
"Gaps not sorted: gap[{i_prev}] = {prev:?}, gap[{i}] = {curr:?}",
i_prev = i - 1,
prev = gaps[i - 1],
curr = gaps[i]
);
}
}
#[test]
fn test_gaps_cover_uncached_space(
cached_ranges in cached_ranges_strategy(),
(query_start, query_end) in block_range_strategy()
) {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let chain = NamedChain::Mainnet;
for (start, end) in &cached_ranges {
cache.insert(from, to, *start, *end, create_test_result(chain, from, to, 1, 1000));
}
let (_, gaps) = cache.calculate_gaps(chain, from, to, query_start, query_end);
let mut covered_blocks = std::collections::HashSet::new();
for (cached_start, cached_end) in &cached_ranges {
let start = (*cached_start).max(query_start);
let end = (*cached_end).min(query_end);
if start <= end {
for block in start..=end {
covered_blocks.insert(block);
}
}
}
for (gap_start, gap_end) in &gaps {
for block in *gap_start..=*gap_end {
covered_blocks.insert(block);
}
}
for block in query_start..=query_end {
prop_assert!(
covered_blocks.contains(&block),
"Block {block} in range [{query_start}, {query_end}] is not covered by cache or gaps"
);
}
}
#[test]
fn test_gaps_dont_overlap_each_other(
cached_ranges in cached_ranges_strategy(),
(query_start, query_end) in block_range_strategy()
) {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let chain = NamedChain::Mainnet;
for (start, end) in &cached_ranges {
cache.insert(from, to, *start, *end, create_test_result(chain, from, to, 1, 1000));
}
let (_, gaps) = cache.calculate_gaps(chain, from, to, query_start, query_end);
for i in 0..gaps.len() {
for j in (i + 1)..gaps.len() {
let (gap_i_start, gap_i_end) = gaps[i];
let (gap_j_start, gap_j_end) = gaps[j];
let no_overlap = gap_i_end < gap_j_start || gap_j_end < gap_i_start;
prop_assert!(
no_overlap,
"Gap {i} [{gap_i_start}, {gap_i_end}] overlaps with gap {j} [{gap_j_start}, {gap_j_end}]"
);
}
}
}
#[test]
fn test_empty_cache_returns_full_range(
(query_start, query_end) in block_range_strategy()
) {
let cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let chain = NamedChain::Mainnet;
let (result, gaps) = cache.calculate_gaps(chain, from, to, query_start, query_end);
prop_assert!(result.is_none(), "Empty cache should return None result");
prop_assert_eq!(gaps.len(), 1, "Empty cache should return exactly one gap");
prop_assert_eq!(gaps[0], (query_start, query_end), "Gap should cover entire query range");
}
#[test]
fn test_fully_cached_returns_no_gaps(
(inner_start, inner_end) in block_range_strategy()
) {
let mut cache = GasCache::default();
let from = Address::ZERO;
let to = Address::ZERO;
let chain = NamedChain::Mainnet;
let cache_start = inner_start.saturating_sub(10);
let cache_end = inner_end.saturating_add(10);
cache.insert(from, to, cache_start, cache_end, create_test_result(chain, from, to, 1, 1000));
let (result, gaps) = cache.calculate_gaps(chain, from, to, inner_start, inner_end);
prop_assert!(result.is_some(), "Fully cached range should return result");
prop_assert_eq!(gaps.len(), 0, "Fully cached range should return no gaps");
}
}
}
}