#![expect(clippy::expect_used, clippy::print_stderr, clippy::print_stdout)]
#![expect(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
use std::collections::HashMap;
use std::env;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use alloy::eips::BlockId;
use alloy::primitives::{address, Address, Bytes, U256};
use alloy::providers::{Provider, ProviderBuilder, RootProvider};
use revm::context::TxEnv;
use revm::database::{AlloyDB, CacheDB};
use revm::database_interface::{DatabaseRef, WrapDatabaseAsync, WrapDatabaseRef};
use revm::primitives::{StorageKey, StorageValue, TxKind, B256};
use revm::{ExecuteEvm, MainBuilder, MainContext};
use degenbot_simulation::{WarmCodeCache, WarmCodeCacheInner};
const DEFAULT_PAIR: Address = address!("B4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc");
const GET_RESERVES: [u8; 4] = [0x09, 0x02, 0xf1, 0xac];
const FANOUT: usize = 50;
const RPC_URL_DEFAULT: &str = "http://host.containers.internal:8545";
#[tokio::main(flavor = "multi_thread")]
async fn main() {
let rpc_url = env::var("RPC").unwrap_or_else(|_| RPC_URL_DEFAULT.to_string());
let pair = env::var("PAIR_ADDRESS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(DEFAULT_PAIR);
eprintln!("RPC URL: {rpc_url}");
eprintln!("Pair: {pair}");
eprintln!("Fan-out: 1 trigger + {FANOUT} read-only calls");
let provider = ProviderBuilder::default().connect_http(rpc_url.parse().expect("valid URL"));
let block_number = provider
.get_block_number()
.await
.expect("RPC reachable: get_block_number failed");
let block_id = BlockId::number(block_number);
eprintln!("Block: {block_number}");
eprintln!();
let calldata = Bytes::from(GET_RESERVES.to_vec());
let tx = build_tx(pair, &calldata);
let cfg_a = run_config_bare(&provider, &tx, block_id);
let cfg_b = run_config_shared_cachedb(&provider, &tx, block_id);
let cfg_c = run_config_proactive(&provider, &tx, block_id);
print_table(&[
("A (bare, no cache)", &cfg_a),
("B (shared CacheDB, transact())", &cfg_b),
("C (proactive, fresh CacheDB/call)", &cfg_c),
]);
eprintln!();
eprintln!(
"=== Config D: cross-block WarmCodeCache (N={WARM_CACHE_N_BLOCKS} blocks, TTL={WARM_CACHE_TTL_BLOCKS}) ==="
);
let start_block = block_number.saturating_sub(WARM_CACHE_N_BLOCKS as u64 - 1);
eprintln!("Sweep: block {start_block} ..= {block_number}");
let cfg_d = run_config_warm_cache_multi_block(
&provider,
&tx,
start_block,
WARM_CACHE_N_BLOCKS,
WARM_CACHE_TTL_BLOCKS,
);
let cfg_b_mb =
run_config_shared_cachedb_multi_block(&provider, &tx, start_block, WARM_CACHE_N_BLOCKS);
print_multi_block_breakdown("D", &cfg_d);
print_multi_block_breakdown("B-noblock", &cfg_b_mb);
let d_total = cfg_d.total_basic + cfg_d.total_storage;
let b_total = cfg_b_mb.total_basic + cfg_b_mb.total_storage;
let saved = b_total.saturating_sub(d_total);
let pct = if b_total == 0 {
0.0
} else {
100.0 * saved as f64 / b_total as f64
};
eprintln!(
"config D total RPCs = {d_total} vs config B (no warm cache) total RPCs = {b_total} over {WARM_CACHE_N_BLOCKS} blocks (saved {saved} RPCs, {pct:.0}% reduction)"
);
assert_warm_cache_ttl_boundary(&cfg_d, WARM_CACHE_TTL_BLOCKS);
eprintln!(
"PASS: TTL boundary re-cold-load verified (basic RPCs: cold at block 1, warm through block {warm}, re-cold at block {cold})",
warm = WARM_CACHE_TTL_BLOCKS + 1,
cold = WARM_CACHE_TTL_BLOCKS + 2
);
}
const CALLER: Address = Address::ZERO;
#[derive(Default)]
struct Counter {
storage_rpcs: AtomicU64,
basic_rpcs: AtomicU64,
}
impl Counter {
fn bump_storage(&self) {
self.storage_rpcs.fetch_add(1, Ordering::Relaxed);
}
fn bump_basic(&self) {
self.basic_rpcs.fetch_add(1, Ordering::Relaxed);
}
fn storage_rpcs(&self) -> u64 {
self.storage_rpcs.load(Ordering::Relaxed)
}
fn basic_rpcs(&self) -> u64 {
self.basic_rpcs.load(Ordering::Relaxed)
}
}
struct CountingRpcDb<Db: DatabaseRef> {
inner: Db,
counter: Arc<Counter>,
}
impl<Db: DatabaseRef> DatabaseRef for CountingRpcDb<Db> {
type Error = Db::Error;
fn storage_ref(&self, address: Address, slot: StorageKey) -> Result<StorageValue, Self::Error> {
self.counter.bump_storage();
self.inner.storage_ref(address, slot)
}
fn basic_ref(&self, address: Address) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
self.counter.bump_basic();
self.inner.basic_ref(address)
}
fn code_by_hash_ref(&self, code_hash: B256) -> Result<revm::bytecode::Bytecode, Self::Error> {
self.inner.code_by_hash_ref(code_hash)
}
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
self.inner.block_hash_ref(number)
}
}
struct AggressiveCachedProvider<Db: DatabaseRef> {
inner: Db,
storage: Mutex<HashMap<(Address, StorageKey), StorageValue>>,
basic: Mutex<HashMap<Address, Option<revm::state::AccountInfo>>>,
counter: Arc<Counter>,
}
impl<Db: DatabaseRef> AggressiveCachedProvider<Db> {
fn new(inner: Db, counter: Arc<Counter>) -> Self {
Self {
inner,
storage: Mutex::new(HashMap::new()),
basic: Mutex::new(HashMap::new()),
counter,
}
}
}
struct CachedRef<Db: DatabaseRef>(Arc<AggressiveCachedProvider<Db>>);
impl<Db: DatabaseRef> Clone for CachedRef<Db> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<Db: DatabaseRef> DatabaseRef for CachedRef<Db> {
type Error = Db::Error;
fn storage_ref(&self, address: Address, slot: StorageKey) -> Result<StorageValue, Self::Error> {
if let Some(hit) = self
.0
.storage
.lock()
.expect("poisoned")
.get(&(address, slot))
{
return Ok(*hit);
}
self.0.counter.bump_storage();
let v = self.0.inner.storage_ref(address, slot)?;
self.0
.storage
.lock()
.expect("poisoned")
.insert((address, slot), v);
Ok(v)
}
fn basic_ref(&self, address: Address) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
if let Some(hit) = self
.0
.basic
.lock()
.expect("poisoned")
.get(&address)
.cloned()
{
return Ok(hit);
}
self.0.counter.bump_basic();
let v = self.0.inner.basic_ref(address)?;
self.0
.basic
.lock()
.expect("poisoned")
.insert(address, v.clone());
Ok(v)
}
fn code_by_hash_ref(&self, code_hash: B256) -> Result<revm::bytecode::Bytecode, Self::Error> {
self.0.inner.code_by_hash_ref(code_hash)
}
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
self.0.inner.block_hash_ref(number)
}
}
struct FundedCallerDb<Db: DatabaseRef> {
inner: Db,
}
impl<Db: DatabaseRef> DatabaseRef for FundedCallerDb<Db> {
type Error = Db::Error;
fn basic_ref(&self, address: Address) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
if address == CALLER {
return Ok(Some(funded_caller_account()));
}
self.inner.basic_ref(address)
}
fn storage_ref(&self, address: Address, slot: StorageKey) -> Result<StorageValue, Self::Error> {
self.inner.storage_ref(address, slot)
}
fn code_by_hash_ref(&self, code_hash: B256) -> Result<revm::bytecode::Bytecode, Self::Error> {
self.inner.code_by_hash_ref(code_hash)
}
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
self.inner.block_hash_ref(number)
}
}
fn funded_caller_account() -> revm::state::AccountInfo {
revm::state::AccountInfo {
balance: U256::MAX,
nonce: 0,
code_hash: revm::primitives::KECCAK_EMPTY,
code: Some(revm::bytecode::Bytecode::default()),
account_id: None,
}
}
struct ConfigResult {
total_wall: Duration,
per_call_ns: Vec<u64>,
storage_rpcs: u64,
basic_rpcs: u64,
}
impl ConfigResult {
fn p50(&self) -> u64 {
percentile(&self.per_call_ns, 50)
}
fn p99(&self) -> u64 {
percentile(&self.per_call_ns, 99)
}
}
fn format_ns(ns: u64) -> String {
if ns < 1_000 {
format!("{ns} ns")
} else if ns < 1_000_000 {
format!("{:.2} µs", ns as f64 / 1_000.0)
} else if ns < 1_000_000_000 {
format!("{:.2} ms", ns as f64 / 1_000_000.0)
} else {
format!("{:.2} s", ns as f64 / 1_000_000_000.0)
}
}
fn format_dur(d: Duration) -> String {
format_ns(d.as_nanos() as u64)
}
fn percentile(samples: &[u64], pct: u8) -> u64 {
if samples.is_empty() {
return 0;
}
let mut sorted = samples.to_vec();
sorted.sort_unstable();
let idx = ((pct as usize * (sorted.len() - 1) + 50) / 100).min(sorted.len() - 1);
sorted[idx]
}
fn print_table(rows: &[(&str, &ConfigResult)]) {
println!(
"| {:<36} | {:>10} | {:>10} | {:>10} | {:>13} | {:>11} |",
"config", "total", "p50", "p99", "storage RPCs", "basic RPCs"
);
println!(
"| {:->36} | {:->10} | {:->10} | {:->10} | {:->13} | {:->11} |",
"", "", "", "", "", ""
);
for (label, r) in rows {
println!(
"| {:<36} | {:>10} | {:>10} | {:>10} | {:>13} | {:>11} |",
label,
format_dur(r.total_wall),
format_ns(r.p50()),
format_ns(r.p99()),
r.storage_rpcs,
r.basic_rpcs
);
}
}
fn build_tx(pair: Address, calldata: &Bytes) -> TxEnv {
TxEnv::builder()
.caller(CALLER)
.kind(TxKind::Call(pair))
.data(calldata.clone())
.value(U256::ZERO)
.gas_limit(100_000)
.gas_price(1)
.build()
.expect("valid TxEnv")
}
fn build_evm<Db: revm::database_interface::Database>(
db: Db,
) -> impl revm::ExecuteEvm<
Tx = TxEnv,
ExecutionResult = revm::context_interface::result::ExecutionResult,
Error: std::fmt::Display,
> {
let mut ctx = revm::context::Context::mainnet();
ctx.cfg.disable_nonce_check = true;
ctx.with_db(db).build_mainnet()
}
fn run_isolated_transacts<E>(
evm: &mut E,
tx: &TxEnv,
n_calls: usize,
print_first_result: bool,
) -> Vec<u64>
where
E: revm::ExecuteEvm<
Tx = TxEnv,
ExecutionResult = revm::context_interface::result::ExecutionResult,
>,
<E as revm::ExecuteEvm>::Error: std::fmt::Display,
{
let mut first_printed = !print_first_result;
(0..n_calls)
.map(|_| {
let start = Instant::now();
let res = evm.transact(tx.clone());
if !first_printed {
first_printed = true;
print_transact_result(&res.map(|r| r.result));
}
start.elapsed().as_nanos() as u64
})
.collect()
}
fn print_transact_result<E: std::fmt::Display>(
res: &Result<revm::context_interface::result::ExecutionResult, E>,
) {
match res {
Ok(r) => {
let kind = match r {
revm::context_interface::result::ExecutionResult::Success { reason, .. } => {
format!("Success({reason:?})")
}
revm::context_interface::result::ExecutionResult::Revert { output, .. } => {
format!("Revert({})", revm::primitives::hex::encode(output))
}
revm::context_interface::result::ExecutionResult::Halt { reason, .. } => {
format!("Halt({reason})")
}
};
let out = r
.output()
.map_or_else(|| "<none>".to_string(), revm::primitives::hex::encode);
eprintln!("{kind} output_len={} output={out}", out.len());
}
Err(e) => eprintln!("Err({e})"),
}
}
fn run_config_bare(provider: &RootProvider, tx: &TxEnv, block_id: BlockId) -> ConfigResult {
let counter = Arc::new(Counter::default());
let counter_clone = counter.clone();
let provider_clone = provider.clone();
let start = Instant::now();
let mut per_call = Vec::with_capacity(1 + FANOUT);
for i in 0..=FANOUT {
let alloy_db = AlloyDB::new(provider_clone.clone(), block_id);
let wrap_db = WrapDatabaseAsync::new(alloy_db).expect("multi-thread runtime");
let counting_db = CountingRpcDb {
inner: wrap_db,
counter: counter_clone.clone(),
};
let funded_db = FundedCallerDb { inner: counting_db };
let db = WrapDatabaseRef(funded_db);
let mut evm = build_evm(db);
let t = Instant::now();
let _ = evm.transact(tx.clone());
per_call.push(t.elapsed().as_nanos() as u64);
if i == 0 {
eprintln!(
"trigger call done: {} storage RPCs, {} basic RPCs",
counter.storage_rpcs(),
counter.basic_rpcs()
);
}
}
ConfigResult {
total_wall: start.elapsed(),
per_call_ns: per_call,
storage_rpcs: counter.storage_rpcs(),
basic_rpcs: counter.basic_rpcs(),
}
}
fn run_config_shared_cachedb(
provider: &RootProvider,
tx: &TxEnv,
block_id: BlockId,
) -> ConfigResult {
let counter = Arc::new(Counter::default());
let alloy_db = AlloyDB::new(provider.clone(), block_id);
let wrap_db = WrapDatabaseAsync::new(alloy_db).expect("multi-thread runtime");
let counting_db = CountingRpcDb {
inner: wrap_db,
counter: counter.clone(),
};
let funded_db = FundedCallerDb { inner: counting_db };
let cache_db = CacheDB::new(funded_db);
let mut evm = build_evm(cache_db);
let start = Instant::now();
let per_call = run_isolated_transacts(&mut evm, tx, 1 + FANOUT, true);
eprintln!(
"after trigger+fan-out: {} storage RPCs, {} basic RPCs",
counter.storage_rpcs(),
counter.basic_rpcs()
);
ConfigResult {
total_wall: start.elapsed(),
per_call_ns: per_call,
storage_rpcs: counter.storage_rpcs(),
basic_rpcs: counter.basic_rpcs(),
}
}
fn run_config_proactive(provider: &RootProvider, tx: &TxEnv, block_id: BlockId) -> ConfigResult {
let counter = Arc::new(Counter::default());
let alloy_db = AlloyDB::new(provider.clone(), block_id);
let wrap_db = WrapDatabaseAsync::new(alloy_db).expect("multi-thread runtime");
let counting_db = CountingRpcDb {
inner: wrap_db,
counter: counter.clone(),
};
let funded_db = FundedCallerDb { inner: counting_db };
let shared = Arc::new(AggressiveCachedProvider::new(funded_db, counter.clone()));
let cached_ref = CachedRef(shared);
let start = Instant::now();
let mut per_call = Vec::with_capacity(1 + FANOUT);
for i in 0..=FANOUT {
let cache_db = CacheDB::new(WrapDatabaseRef(cached_ref.clone()));
let mut evm = build_evm(cache_db);
let t = Instant::now();
let _ = evm.transact(tx.clone());
per_call.push(t.elapsed().as_nanos() as u64);
if i == 0 {
eprintln!(
"trigger call done: {} storage RPCs, {} basic RPCs",
counter.storage_rpcs(),
counter.basic_rpcs()
);
}
}
ConfigResult {
total_wall: start.elapsed(),
per_call_ns: per_call,
storage_rpcs: counter.storage_rpcs(),
basic_rpcs: counter.basic_rpcs(),
}
}
const WARM_CACHE_TTL_BLOCKS: u64 = 10;
const WARM_CACHE_N_BLOCKS: usize = WARM_CACHE_TTL_BLOCKS as usize + 2;
#[derive(Clone, Copy)]
struct PerBlockRpc {
ordinal: usize,
block_number: u64,
basic_rpcs: u64,
storage_rpcs: u64,
}
struct MultiBlockResult {
per_block: Vec<PerBlockRpc>,
total_basic: u64,
total_storage: u64,
#[expect(dead_code)]
total_wall: Duration,
}
fn run_config_warm_cache_multi_block(
provider: &RootProvider,
tx: &TxEnv,
start_block: u64,
n_blocks: usize,
ttl_blocks: u64,
) -> MultiBlockResult {
let counter = Arc::new(Counter::default());
let warm_inner: Arc<parking_lot::RwLock<WarmCodeCacheInner>> =
WarmCodeCacheInner::shared_with_ttl(ttl_blocks);
let start = Instant::now();
let mut per_block = Vec::with_capacity(n_blocks);
for i in 0..n_blocks {
let block_number = start_block + i as u64;
let before_basic = counter.basic_rpcs();
let before_storage = counter.storage_rpcs();
let alloy_db = AlloyDB::new(provider.clone(), BlockId::number(block_number));
let wrap_db = WrapDatabaseAsync::new(alloy_db).expect("multi-thread runtime");
let counting_db = CountingRpcDb {
inner: wrap_db,
counter: counter.clone(),
};
let funded_db = FundedCallerDb { inner: counting_db };
let warm_db = WarmCodeCache::with_owner(warm_inner.clone(), block_number, funded_db);
let cache_db = CacheDB::new(warm_db);
let mut evm = build_evm(cache_db);
let _ = evm.transact(tx.clone());
per_block.push(PerBlockRpc {
ordinal: i + 1,
block_number,
basic_rpcs: counter.basic_rpcs() - before_basic,
storage_rpcs: counter.storage_rpcs() - before_storage,
});
}
MultiBlockResult {
per_block,
total_basic: counter.basic_rpcs(),
total_storage: counter.storage_rpcs(),
total_wall: start.elapsed(),
}
}
fn run_config_shared_cachedb_multi_block(
provider: &RootProvider,
tx: &TxEnv,
start_block: u64,
n_blocks: usize,
) -> MultiBlockResult {
let counter = Arc::new(Counter::default());
let start = Instant::now();
let mut per_block = Vec::with_capacity(n_blocks);
for i in 0..n_blocks {
let block_number = start_block + i as u64;
let before_basic = counter.basic_rpcs();
let before_storage = counter.storage_rpcs();
let alloy_db = AlloyDB::new(provider.clone(), BlockId::number(block_number));
let wrap_db = WrapDatabaseAsync::new(alloy_db).expect("multi-thread runtime");
let counting_db = CountingRpcDb {
inner: wrap_db,
counter: counter.clone(),
};
let funded_db = FundedCallerDb { inner: counting_db };
let cache_db = CacheDB::new(funded_db);
let mut evm = build_evm(cache_db);
let _ = evm.transact(tx.clone());
per_block.push(PerBlockRpc {
ordinal: i + 1,
block_number,
basic_rpcs: counter.basic_rpcs() - before_basic,
storage_rpcs: counter.storage_rpcs() - before_storage,
});
}
MultiBlockResult {
per_block,
total_basic: counter.basic_rpcs(),
total_storage: counter.storage_rpcs(),
total_wall: start.elapsed(),
}
}
fn print_multi_block_breakdown(label: &str, r: &MultiBlockResult) {
eprintln!();
eprintln!("[{label}] per-block RPC breakdown:");
eprintln!(
" {:>6} {:>14} {:>12} {:>14}",
"block", "number", "basic RPCs", "storage RPCs"
);
for b in &r.per_block {
eprintln!(
" {:>6} {:>14} {:>12} {:>14}",
b.ordinal, b.block_number, b.basic_rpcs, b.storage_rpcs
);
}
let total = r.total_basic + r.total_storage;
eprintln!(
"[{label}] total: {} basic + {} storage = {} RPCs over {} blocks",
r.total_basic,
r.total_storage,
total,
r.per_block.len()
);
}
fn assert_warm_cache_ttl_boundary(r: &MultiBlockResult, ttl_blocks: u64) {
let n = r.per_block.len();
let ttl = ttl_blocks as usize;
let first_stale_ordinal = ttl + 2; assert_eq!(
n, first_stale_ordinal,
"config D run length must equal ttl_blocks + 2 to capture the TTL boundary"
);
assert_eq!(
r.per_block[0].basic_rpcs, 1,
"block 1 should cold-load basic (expected 1 basic RPC)"
);
for b in &r.per_block[1..first_stale_ordinal - 1] {
assert_eq!(
b.basic_rpcs, 0,
"block {} should hit the warm cache (expected 0 basic RPCs)",
b.ordinal
);
}
let stale = &r.per_block[first_stale_ordinal - 1];
assert_eq!(
stale.basic_rpcs, 1,
"block {} (TTL boundary, loaded at 1) should re-cold-load basic (expected 1 basic RPC)",
stale.ordinal
);
for b in &r.per_block {
assert_eq!(
b.storage_rpcs, 1,
"block {} should fire 1 storage RPC (storage is never cached)",
b.ordinal
);
}
assert_eq!(
r.total_basic, 2,
"config D should fire basic RPC exactly twice (cold load + TTL re-cold)"
);
}