use alloy::primitives::Address;
use degenbot_bot::bot_core::SimAnchorState;
use revm::database_interface::DatabaseRef;
use revm::primitives::{StorageKey, StorageValue, B256, KECCAK_EMPTY};
use revm::state::AccountInfo;
pub struct BotStateDb<'bot, ExtDb>
where
ExtDb: DatabaseRef,
{
pub anchor: &'bot SimAnchorState,
pub fallback: ExtDb,
code_probe_rpc: Option<String>,
code_probe_block: Option<u64>,
}
impl<'bot, ExtDb> BotStateDb<'bot, ExtDb>
where
ExtDb: DatabaseRef,
{
#[must_use]
pub fn new(anchor: &'bot SimAnchorState, fallback: ExtDb) -> Self {
Self {
anchor,
fallback,
code_probe_rpc: None,
code_probe_block: None,
}
}
#[must_use]
pub fn new_with_code_probe(
anchor: &'bot SimAnchorState,
fallback: ExtDb,
rpc_url: &str,
sim_block: u64,
) -> Self {
Self {
anchor,
fallback,
code_probe_rpc: Some(rpc_url.to_string()),
code_probe_block: Some(sim_block),
}
}
fn code_probe_provenance(&self, address: Address, info: Option<&AccountInfo>) -> String {
let acct = match info {
Some(a) => format!(
"fallback_account: balance={} nonce={} code_hash={}",
a.balance, a.nonce, a.code_hash
),
None => "fallback_account: None (read as non-existent)".to_string(),
};
let absent_hint = match info {
Some(a) if a.balance.is_zero() && a.nonce == 0 => {
" [ACCOUNT-ABSENT: balance=0 nonce=0 — consistent with a STALE/BAD block where \
the contract was not yet deployed or served as an EOA]"
}
_ => "",
};
let (Some(rpc), Some(block)) = (&self.code_probe_rpc, self.code_probe_block) else {
return format!("{acct};; no-probe (code probe unconfigured)");
};
let fresh = raw_uncached_eth_get_code(rpc, address, block);
match fresh {
Some(code) if code.len() > 2 && code != "0x" => format!(
"{acct}{absent_hint};; FRESH UNCACHED GET eth_getCode({address},\
0x{block:x}) => {:x}-byte code -> CACHE-SERVED EMPTY (the alloy \
with_default_caching() LRU persisted a spurious 0x; the node HAS the pool\
at THIS block, so the empty is NOT a stale-block issue on the live read)",
(code.len() - 2) / 2
),
Some(code) => format!(
"{acct}{absent_hint};; FRESH UNCACHED GET eth_getCode({address},\
{block}) => ALSO EMPTY ({code:?}) -> NODE/BLOCK-ORIGIN (the live node returns\
empty at this exact block — a stale/bad block WOULD explain this; cross-check\
the block against live head)"
),
None => format!(
"{acct};; PROBE-FAILED — raw eth_getCode HTTP error (cannot attribute layer)"
),
}
}
}
impl<ExtDb> DatabaseRef for BotStateDb<'_, ExtDb>
where
ExtDb: DatabaseRef,
{
type Error = ExtDb::Error;
#[expect(clippy::panic_in_result_fn, clippy::panic)] fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
let info = self.fallback.basic_ref(address)?;
if self.anchor.pool_id_by_address(&address).is_some() {
let invalid = match &info {
None => true,
Some(acc) => acc.code_hash == KECCAK_EMPTY,
};
if invalid {
let state = if info.is_none() {
"non-existent (None)"
} else {
"code-less (KECCAK_EMPTY)"
};
let provenance = self.code_probe_provenance(address, info.as_ref());
tracing::warn!(
"[codeless-probe] tracked pool {address} resolved {state} @ block {:?}; {provenance}",
self.code_probe_block,
);
panic!(
"Sim DB invariant: tracked pool {address} resolved as {state} by the \
RPC fallback — refusing to simulate a code-less pool"
);
}
}
Ok(info)
}
fn storage_ref(
&self,
address: Address,
index: StorageKey,
) -> Result<StorageValue, Self::Error> {
let rpc_value = self.fallback.storage_ref(address, index)?;
super::divergence_probe::observe_storage_read(self.anchor, address, index, rpc_value);
let served = super::serving::serve_tracked_slot(self.anchor, address, index, rpc_value);
Ok(served.unwrap_or(rpc_value))
}
fn code_by_hash_ref(&self, code_hash: B256) -> Result<revm::bytecode::Bytecode, Self::Error> {
self.fallback.code_by_hash_ref(code_hash)
}
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
self.fallback.block_hash_ref(number)
}
}
fn raw_uncached_eth_get_code(rpc_url: &str, address: Address, block: u64) -> Option<String> {
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
let no_scheme = rpc_url.strip_prefix("http://")?;
let (host, port) = match no_scheme.split_once(':') {
Some((h, p)) => (h.to_string(), p.parse::<u16>().ok()?),
None => (no_scheme.to_string(), 80u16),
};
let body = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["{address}","0x{block:x}"]}}"#
);
let req = format!(
"POST / HTTP/1.1\r\nHost: {host}:{port}\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
let mut stream = TcpStream::connect((host.as_str(), port)).ok()?;
stream.set_read_timeout(Some(Duration::from_secs(5))).ok()?;
stream.write_all(req.as_bytes()).ok()?;
let mut resp = String::new();
stream.read_to_string(&mut resp).ok()?;
let body_json = resp.split("\r\n\r\n").nth(1)?;
let v: serde_json::Value = serde_json::from_str(body_json).ok()?;
v.get("result")?.as_str().map(str::to_string)
}
#[expect(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::aliases::U112;
use alloy::primitives::{Address, U256};
use degenbot_bot::bot_core::{BotState, RegisterV2PoolParams};
use degenbot_uniswap::dex_identity::DexVariant;
use revm::bytecode::Bytecode;
use revm::primitives::B256;
use std::cell::Cell;
const POOL: Address = Address::new([0xaa; 20]);
const EOA: Address = Address::new([0xee; 20]);
fn bot_state_with_pool() -> BotState {
let mut core = BotState::new();
core.register_v2_pool(&RegisterV2PoolParams {
address: POOL,
token0: Address::from([0xbb; 20]),
token1: Address::from([0xcc; 20]),
reserve0: U112::from(1000),
reserve1: U112::from(2000),
fee_token0: (997, 1000),
fee_token1: (997, 1000),
factory: Address::from([0xdd; 20]),
update_block: 0,
variant: DexVariant::UniswapV2,
stable_swap: false,
fee_denominator: None,
..Default::default()
})
.expect("test setup: V2 registration");
core
}
struct ScriptedDb {
result: Option<AccountInfo>,
calls: Cell<u64>,
}
impl DatabaseRef for ScriptedDb {
type Error = core::convert::Infallible;
fn basic_ref(&self, _address: Address) -> Result<Option<AccountInfo>, Self::Error> {
self.calls.set(self.calls.get() + 1);
Ok(self.result.clone())
}
fn code_by_hash_ref(&self, _code_hash: B256) -> Result<Bytecode, Self::Error> {
Ok(Bytecode::new_legacy(alloy::primitives::Bytes::new()))
}
fn storage_ref(
&self,
_address: Address,
_index: StorageKey,
) -> Result<StorageValue, Self::Error> {
Ok(StorageValue::ZERO)
}
fn block_hash_ref(&self, _number: u64) -> Result<B256, Self::Error> {
Ok(B256::ZERO)
}
}
fn code_info() -> AccountInfo {
let code = Bytecode::new_legacy(alloy::primitives::Bytes::from_static(&[0x60, 0x00]));
AccountInfo::new(U256::from(1), 0, code.hash_slow(), code)
}
fn codeless_info() -> AccountInfo {
AccountInfo::new(U256::from(1), 0, KECCAK_EMPTY, Bytecode::default())
}
#[test]
#[should_panic(expected = "Sim DB invariant")]
fn tracked_pool_resolving_none_panics() {
let core = bot_state_with_pool();
let db = ScriptedDb {
result: None,
calls: Cell::new(0),
};
let anchor = SimAnchorState::snapshot(&core);
let bsd = BotStateDb::new(&anchor, db);
let _ = bsd.basic_ref(POOL).unwrap();
}
#[test]
#[should_panic(expected = "Sim DB invariant")]
fn tracked_pool_resolving_codeless_panics() {
let core = bot_state_with_pool();
let db = ScriptedDb {
result: Some(codeless_info()),
calls: Cell::new(0),
};
let anchor = SimAnchorState::snapshot(&core);
let bsd = BotStateDb::new(&anchor, db);
let _ = bsd.basic_ref(POOL).unwrap();
}
#[test]
fn tracked_pool_with_code_passes_through() {
let core = bot_state_with_pool();
let db = ScriptedDb {
result: Some(code_info()),
calls: Cell::new(0),
};
let anchor = SimAnchorState::snapshot(&core);
let bsd = BotStateDb::new(&anchor, db);
let got = bsd.basic_ref(POOL).unwrap();
assert!(got.is_some(), "tracked pool with code is forwarded");
assert_ne!(got.unwrap().code_hash, KECCAK_EMPTY);
}
#[test]
fn non_pool_none_is_exempt() {
let core = bot_state_with_pool();
let db = ScriptedDb {
result: None,
calls: Cell::new(0),
};
let anchor = SimAnchorState::snapshot(&core);
let bsd = BotStateDb::new(&anchor, db);
assert!(bsd.basic_ref(EOA).unwrap().is_none());
}
#[test]
fn raw_uncached_eth_get_code_reads_fresh_code_past_the_cache() {
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let serve = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
let mut buf = [0u8; 4096];
let _ = sock.read(&mut buf);
let body = r#"{"jsonrpc":"2.0","id":1,"result":"0x608060405234"}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
sock.write_all(resp.as_bytes()).unwrap();
});
let got = raw_uncached_eth_get_code(
&format!("http://127.0.0.1:{port}"),
parse_addr(),
1_234_567u64,
);
serve.join().unwrap();
assert_eq!(
got.as_deref(),
Some("0x608060405234"),
"probe reads fresh code straight off the wire, uncached"
);
}
fn parse_addr() -> Address {
"0x36D2b521d708537B98F01Ab8d5207BD8E42b2806"
.parse()
.unwrap()
}
#[test]
fn stale_block_signature_reports_node_origin_and_account_absent() {
use std::io::{Read, Write};
use std::net::TcpListener;
use std::thread;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
let serve = thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
let mut buf = [0u8; 4096];
let _ = sock.read(&mut buf);
let body = r#"{"jsonrpc":"2.0","id":1,"result":"0x"}"#;
let resp = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\
Content-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body
);
sock.write_all(resp.as_bytes()).unwrap();
});
let core = bot_state_with_pool();
let anchor = SimAnchorState::snapshot(&core);
let db = ScriptedDb {
result: Some(codeless_info()),
calls: Cell::new(0),
};
let bsd = BotStateDb::new_with_code_probe(
&anchor,
db,
&format!("http://127.0.0.1:{port}"),
1_234_567u64,
);
let mut empty_acct = codeless_info();
empty_acct.balance = U256::ZERO;
let verdict = bsd.code_probe_provenance(POOL, Some(&empty_acct));
serve.join().unwrap();
assert!(
verdict.contains("ACCOUNT-ABSENT"),
"hint missing: {verdict}"
);
assert!(
verdict.contains("NODE/BLOCK-ORIGIN"),
"node/block verdict missing: {verdict}"
);
}
}