mod binary_state;
mod bytecode;
mod code_seeds;
mod journal_access_list;
mod metadata;
pub mod overlay;
pub mod slot_observations;
pub mod snapshot;
pub(crate) mod versioned;
pub use binary_state::{load_binary_state, save_binary_state};
pub use metadata::{CacheConfig, ImmutableDataCache};
pub use overlay::EvmOverlay;
pub use slot_observations::SlotObservationTracker;
pub use snapshot::EvmSnapshot;
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
fs,
rc::Rc,
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use alloy_consensus::BlockHeader;
use alloy_eips::eip2930::AccessList;
use alloy_eips::{BlockId, BlockNumberOrTag};
use alloy_network::BlockResponse;
use alloy_primitives::{Address, B256, Bytes, I256, Log, TxKind, U256, keccak256};
use alloy_provider::{Provider, network::AnyNetwork};
use alloy_rpc_types_eth::TransactionRequest;
use alloy_sol_types::{SolCall, SolValue, sol};
use foundry_fork_db::{BlockchainDb, SharedBackend, cache::BlockchainDbMeta};
use revm::{
Context, ExecuteCommitEvm, ExecuteEvm, InspectEvm, MainBuilder, MainContext,
context::{BlockEnv, CfgEnv, Journal, LocalContext, TxEnv, result::ExecutionResult},
context_interface::JournalTr,
database::{AccountState, CacheDB},
primitives::hardfork::SpecId,
state::{Account, AccountInfo, Bytecode},
};
use tracing::{debug, instrument, trace, warn};
use crate::access_set::StorageAccessList;
use crate::bulk_storage::AccountFieldsSample;
use crate::errors::{
BlockContextError, CacheError, CacheResult as Result, RpcError, RuntimeError, SimError,
SimHostError, SimulationError, SimulationResult, StorageFetchError, StorageFetchResult,
};
use crate::freshness::{SlotChange, SlotFetch, SlotOutcome};
use crate::inspector::TransferInspector;
use crate::mapping_probe::{
HashSlotAccess, HashStorageProbe, SlotLayout, TrackedBalances, TrackedMapping,
};
use crate::state_update::{
AccountChange, AccountPatch, PurgeRecord, PurgeScope, SkippedAccountPatch, SkippedBalanceDelta,
SkippedDelta, SkippedMask, SlotDelta, StateDiff, StateUpdate,
};
use bytecode::BytecodeCache;
use code_seeds::CodeSeedCache;
pub use code_seeds::CodeSeedState;
use journal_access_list::{extract_access_list, merge_access_lists};
pub use alloy_provider::network::AnyNetwork as AnyNetworkType;
pub type ForkCacheDB = CacheDB<SharedBackend>;
pub type RpcCallFn = Arc<dyn Fn(Address, Bytes) -> Result<Bytes, RpcError> + Send + Sync>;
pub type StorageBatchFetchFn = Arc<
dyn Fn(Vec<(Address, U256)>, BlockId) -> Vec<(Address, U256, StorageFetchResult<U256>)>
+ Send
+ Sync,
>;
#[derive(Clone, Debug)]
pub struct AccountProof {
pub storage_hash: B256,
pub balance: U256,
pub nonce: u64,
pub code_hash: B256,
pub slots: Vec<(U256, U256)>,
}
pub type AccountProofFetchFn = Arc<
dyn Fn(Vec<(Address, Vec<U256>)>, BlockId) -> Vec<(Address, StorageFetchResult<AccountProof>)>
+ Send
+ Sync,
>;
pub type AccountFieldsFetchFn = Arc<
dyn Fn(Vec<Address>, BlockId) -> StorageFetchResult<Vec<(Address, AccountFieldsSample)>>
+ Send
+ Sync,
>;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BlockStateDiff {
pub accounts: Vec<BlockStateAccountDiff>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlockStateAccountDiff {
pub address: Address,
pub balance: Option<U256>,
pub nonce: Option<u64>,
pub code: Option<Bytes>,
pub storage: Vec<BlockStateStorageDiff>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlockStateStorageDiff {
pub slot: U256,
pub value: U256,
}
pub type BlockStateDiffFetchFn =
Arc<dyn Fn(BlockId) -> StorageFetchResult<BlockStateDiff> + Send + Sync>;
pub(crate) fn block_in_place_handle() -> Result<tokio::runtime::Handle, RuntimeError> {
match tokio::runtime::Handle::try_current() {
Ok(handle) => match handle.runtime_flavor() {
tokio::runtime::RuntimeFlavor::CurrentThread => Err(RuntimeError::CurrentThreadRuntime),
_ => Ok(handle),
},
Err(e) => Err(RuntimeError::MissingRuntime {
details: e.to_string(),
}),
}
}
fn trace_rpc_method_and_params(block: BlockId) -> (&'static str, serde_json::Value) {
let tracer = serde_json::json!({
"tracer": "prestateTracer",
"tracerConfig": {
"diffMode": true,
},
});
match block {
BlockId::Hash(hash) => (
"debug_traceBlockByHash",
serde_json::json!([hash.block_hash, tracer]),
),
BlockId::Number(number) => (
"debug_traceBlockByNumber",
serde_json::json!([block_number_or_tag_param(number), tracer]),
),
}
}
fn block_number_or_tag_param(number: BlockNumberOrTag) -> serde_json::Value {
match number {
BlockNumberOrTag::Number(number) => serde_json::json!(format!("{number:#x}")),
BlockNumberOrTag::Latest => serde_json::json!("latest"),
BlockNumberOrTag::Finalized => serde_json::json!("finalized"),
BlockNumberOrTag::Safe => serde_json::json!("safe"),
BlockNumberOrTag::Earliest => serde_json::json!("earliest"),
BlockNumberOrTag::Pending => serde_json::json!("pending"),
}
}
fn parse_block_state_diff_trace(value: &serde_json::Value) -> Result<BlockStateDiff> {
let mut accounts: HashMap<Address, BlockStateAccountDiff> = HashMap::new();
match value {
serde_json::Value::Array(traces) => {
for trace in traces {
merge_trace_diff(trace, &mut accounts)?;
}
}
trace => merge_trace_diff(trace, &mut accounts)?,
}
let mut accounts: Vec<_> = accounts.into_values().collect();
accounts.sort_by_key(|account| account.address);
for account in &mut accounts {
account.storage.sort_by_key(|slot| slot.slot);
}
Ok(BlockStateDiff { accounts })
}
fn merge_trace_diff(
trace: &serde_json::Value,
accounts: &mut HashMap<Address, BlockStateAccountDiff>,
) -> Result<()> {
let diff = trace.get("result").unwrap_or(trace);
let Some(pre) = diff.get("pre").and_then(serde_json::Value::as_object) else {
return Ok(());
};
let Some(post) = diff.get("post").and_then(serde_json::Value::as_object) else {
return Ok(());
};
for (address, post_account) in post {
let address = parse_trace_address(address)?;
let entry = accounts
.entry(address)
.or_insert_with(|| empty_block_state_account_diff(address));
if let Some(balance) = post_account.get("balance") {
entry.balance = Some(parse_trace_u256(balance)?);
}
if let Some(nonce) = post_account.get("nonce") {
entry.nonce = Some(parse_trace_u64(nonce)?);
}
if let Some(code) = post_account.get("code") {
entry.code = Some(parse_trace_bytes(code)?);
}
if let Some(storage) = post_account
.get("storage")
.and_then(serde_json::Value::as_object)
{
for (slot, value) in storage {
upsert_block_state_storage_diff(
entry,
parse_trace_u256_str(slot)?,
parse_trace_u256(value)?,
);
}
}
}
for (address_key, pre_account) in pre {
let address = parse_trace_address(address_key)?;
let post_account = post.get(address_key);
if post_account.is_none() {
let entry = accounts
.entry(address)
.or_insert_with(|| empty_block_state_account_diff(address));
entry.balance = Some(U256::ZERO);
entry.nonce = Some(0);
entry.code = Some(Bytes::new());
}
let Some(pre_storage) = pre_account
.get("storage")
.and_then(serde_json::Value::as_object)
else {
continue;
};
let post_storage = post_account
.and_then(|account| account.get("storage"))
.and_then(serde_json::Value::as_object);
for slot in pre_storage.keys() {
let cleared = post_storage.is_none_or(|storage| !storage.contains_key(slot));
if cleared {
let entry = accounts
.entry(address)
.or_insert_with(|| empty_block_state_account_diff(address));
upsert_block_state_storage_diff(entry, parse_trace_u256_str(slot)?, U256::ZERO);
}
}
}
Ok(())
}
fn empty_block_state_account_diff(address: Address) -> BlockStateAccountDiff {
BlockStateAccountDiff {
address,
balance: None,
nonce: None,
code: None,
storage: Vec::new(),
}
}
fn upsert_block_state_storage_diff(account: &mut BlockStateAccountDiff, slot: U256, value: U256) {
if let Some(existing) = account.storage.iter_mut().find(|entry| entry.slot == slot) {
existing.value = value;
} else {
account.storage.push(BlockStateStorageDiff { slot, value });
}
}
fn parse_trace_address(value: &str) -> Result<Address> {
value.parse().map_err(|err| CacheError::TraceParse {
details: format!("invalid address `{value}`: {err}"),
})
}
fn parse_trace_u256(value: &serde_json::Value) -> Result<U256> {
match value {
serde_json::Value::String(value) => parse_trace_u256_str(value),
serde_json::Value::Number(value) => parse_trace_u256_str(&value.to_string()),
other => Err(CacheError::TraceParse {
details: format!("expected U256 string/number, got {other:?}"),
}),
}
}
fn parse_trace_u256_str(value: &str) -> Result<U256> {
if let Some(value) = value.strip_prefix("0x") {
if value.is_empty() {
return Ok(U256::ZERO);
}
return U256::from_str_radix(value, 16).map_err(|err| CacheError::TraceParse {
details: format!("invalid U256 `0x{value}`: {err}"),
});
}
if value.is_empty() {
return Ok(U256::ZERO);
}
U256::from_str_radix(value, 10).map_err(|err| CacheError::TraceParse {
details: format!("invalid U256 `{value}`: {err}"),
})
}
fn parse_trace_u64(value: &serde_json::Value) -> Result<u64> {
match value {
serde_json::Value::Number(value) => value.as_u64().ok_or_else(|| CacheError::TraceParse {
details: format!("invalid u64 number `{value}`"),
}),
serde_json::Value::String(value) => {
if let Some(value) = value.strip_prefix("0x") {
if value.is_empty() {
return Ok(0);
}
return u64::from_str_radix(value, 16).map_err(|err| CacheError::TraceParse {
details: format!("invalid u64 `0x{value}`: {err}"),
});
}
if value.is_empty() {
return Ok(0);
}
value.parse().map_err(|err| CacheError::TraceParse {
details: format!("invalid u64 `{value}`: {err}"),
})
}
other => Err(CacheError::TraceParse {
details: format!("expected u64 string/number, got {other:?}"),
}),
}
}
fn parse_trace_bytes(value: &serde_json::Value) -> Result<Bytes> {
let Some(value) = value.as_str() else {
return Err(CacheError::TraceParse {
details: format!("expected bytecode string, got {value:?}"),
});
};
let value = value.strip_prefix("0x").unwrap_or(value);
let bytes = alloy_primitives::hex::decode(value).map_err(|err| CacheError::TraceParse {
details: format!("invalid bytecode hex: {err}"),
})?;
Ok(Bytes::from(bytes))
}
pub(crate) fn unix_timestamp_secs_saturating(time: SystemTime) -> u64 {
time.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_secs())
.unwrap_or(0)
}
fn read_slot_account_state_aware<S1, S2>(
overlay: &std::collections::HashMap<Address, revm::database::DbAccount, S1>,
storage: &std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
address: Address,
slot: U256,
) -> Option<U256>
where
S1: std::hash::BuildHasher,
S2: std::hash::BuildHasher,
{
if let Some(db_account) = overlay.get(&address) {
if let Some(value) = db_account.storage.get(&slot) {
return Some(*value);
}
if matches!(
db_account.account_state,
AccountState::StorageCleared | AccountState::NotExisting
) {
return Some(U256::ZERO);
}
}
storage.get(&address).and_then(|s| s.get(&slot).copied())
}
fn write_slot_into<S1, S2>(
overlay: &mut std::collections::HashMap<Address, revm::database::DbAccount, S1>,
storage: &mut std::collections::HashMap<Address, foundry_fork_db::cache::StorageInfo, S2>,
address: Address,
slot: U256,
value: U256,
) where
S1: std::hash::BuildHasher,
S2: std::hash::BuildHasher + Default,
{
storage.entry(address).or_default().insert(slot, value);
if let Some(db_account) = overlay.get_mut(&address) {
db_account.storage.insert(slot, value);
}
}
fn account_patch_is_empty(patch: &AccountPatch) -> bool {
patch.balance.is_none() && patch.nonce.is_none() && patch.code.is_none()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(u8)]
pub enum CacheSpeedMode {
Fast = 0,
Normal = 1,
#[default]
Slow = 2,
XSlow = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StorageBatchConfig {
pub slots_per_batch: usize,
pub max_concurrent_batches: usize,
}
impl StorageBatchConfig {
pub fn new(slots_per_batch: usize, max_concurrent_batches: usize) -> Self {
Self {
slots_per_batch,
max_concurrent_batches,
}
.normalized()
}
fn normalized(self) -> Self {
Self {
slots_per_batch: self.slots_per_batch.max(1),
max_concurrent_batches: self.max_concurrent_batches.max(1),
}
}
}
impl Default for StorageBatchConfig {
fn default() -> Self {
CacheSpeedMode::default().into()
}
}
impl From<CacheSpeedMode> for StorageBatchConfig {
fn from(mode: CacheSpeedMode) -> Self {
match mode {
CacheSpeedMode::Fast => Self::new(150, 8),
CacheSpeedMode::Normal => Self::new(100, 6),
CacheSpeedMode::Slow => Self::new(75, 4),
CacheSpeedMode::XSlow => Self::new(25, 1),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageFetchStrategy {
BulkCall(crate::bulk_storage::BulkCallConfig),
PointRead,
}
impl Default for StorageFetchStrategy {
fn default() -> Self {
Self::BulkCall(crate::bulk_storage::BulkCallConfig::default())
}
}
pub fn point_read_storage_fetcher<P>(
provider: Arc<P>,
config: StorageBatchConfig,
) -> StorageBatchFetchFn
where
P: Provider<AnyNetwork> + 'static,
{
let config = config.normalized();
Arc::new(
move |requests: Vec<(Address, U256)>, current_block: BlockId| {
use futures::stream::{self, StreamExt};
let batch_size = config.slots_per_batch;
let max_concurrent = config.max_concurrent_batches;
let handle = match block_in_place_handle() {
Ok(handle) => handle,
Err(e) => {
return requests
.into_iter()
.map(|(addr, slot)| {
(
addr,
slot,
Err(StorageFetchError::Runtime(RuntimeError::MissingRuntime {
details: e.to_string(),
})),
)
})
.collect();
}
};
tokio::task::block_in_place(|| {
handle.block_on(async {
let mut results = Vec::with_capacity(requests.len());
let batch_futs: Vec<_> = requests
.chunks(batch_size)
.map(|chunk| {
let client = provider.client();
let mut batch = alloy_rpc_client::BatchRequest::new(client);
let mut waiters = Vec::with_capacity(chunk.len());
for &(addr, slot) in chunk {
let params = (addr, slot, current_block);
match batch.add_call::<_, U256>("eth_getStorageAt", ¶ms) {
Ok(waiter) => waiters.push((addr, slot, Ok(waiter))),
Err(e) => {
tracing::warn!(
?addr,
?slot,
"batch request serialization failed: {}",
e
);
waiters.push((
addr,
slot,
Err(StorageFetchError::serialization(e)),
));
}
}
}
async move {
let send_result = batch.send().await;
let mut chunk_results = Vec::with_capacity(waiters.len());
let batch_error =
send_result.as_ref().err().map(|err| err.to_string());
for (addr, slot, waiter) in waiters {
match waiter {
Ok(waiter) => {
if let Some(source) = &batch_error {
chunk_results.push((
addr,
slot,
Err(StorageFetchError::batch_send(source)),
));
continue;
}
match waiter.await {
Ok(value) => {
chunk_results.push((addr, slot, Ok(value)));
}
Err(e) => {
chunk_results.push((
addr,
slot,
Err(StorageFetchError::provider(
"eth_getStorageAt",
e,
)),
));
}
}
}
Err(err) => {
chunk_results.push((addr, slot, Err(err)));
}
}
}
chunk_results
}
})
.collect();
let all_batch_results: Vec<Vec<_>> = stream::iter(batch_futs)
.buffer_unordered(max_concurrent)
.collect()
.await;
for batch_results in all_batch_results {
results.extend(batch_results);
}
results
})
})
},
)
}
#[derive(Debug, Default)]
pub struct PrewarmReport {
pub loaded: usize,
pub failed: Vec<(Address, U256, StorageFetchError)>,
}
#[derive(Clone, Debug, Default)]
pub struct CodeVerifyReport {
pub verified: Vec<Address>,
pub mismatched: Vec<CodeMismatch>,
pub not_deployed: Vec<Address>,
pub codeless: Vec<Address>,
pub unverifiable: Vec<(Address, String)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CodeMismatch {
pub address: Address,
pub expected: B256,
pub actual: B256,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissingTargetBehavior {
Error,
Create,
}
#[derive(Debug, Clone, Default)]
pub struct TxConfig {
pub value: U256,
pub gas_limit: Option<u64>,
pub gas_price: Option<u128>,
pub nonce: Option<u64>,
pub access_list: Option<AccessList>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockContextRequirements {
pub require_number: bool,
pub require_basefee: bool,
pub require_coinbase: bool,
pub require_prevrandao: bool,
pub require_gas_limit: bool,
}
impl Default for BlockContextRequirements {
fn default() -> Self {
Self::lenient()
}
}
impl BlockContextRequirements {
pub const fn strict() -> Self {
Self {
require_number: true,
require_basefee: true,
require_coinbase: true,
require_prevrandao: true,
require_gas_limit: true,
}
}
pub const fn lenient() -> Self {
Self {
require_number: false,
require_basefee: false,
require_coinbase: false,
require_prevrandao: false,
require_gas_limit: false,
}
}
pub fn validate_header<H: BlockHeader>(&self, header: &H) -> Result<(), BlockContextError> {
if self.require_basefee && header.base_fee_per_gas().is_none() {
return Err(BlockContextError::MissingField { field: "basefee" });
}
if self.require_prevrandao && header.mix_hash().is_none() {
return Err(BlockContextError::MissingField {
field: "prevrandao",
});
}
Ok(())
}
}
pub struct EvmCacheBuilder<P> {
provider: Arc<P>,
block: BlockId,
cache_config: Option<CacheConfig>,
spec_id: SpecId,
shared_memory_capacity: SharedMemoryCapacity,
storage_batch_config: StorageBatchConfig,
storage_fetch_strategy: StorageFetchStrategy,
chain_id: Option<u64>,
block_context_requirements: BlockContextRequirements,
max_concurrent_proofs: usize,
}
impl<P> EvmCacheBuilder<P>
where
P: Provider<AnyNetwork> + 'static,
{
pub fn new(provider: Arc<P>) -> Self {
Self {
provider,
block: BlockId::latest(),
cache_config: None,
spec_id: SpecId::CANCUN,
shared_memory_capacity: SharedMemoryCapacity::default(),
storage_batch_config: StorageBatchConfig::default(),
storage_fetch_strategy: StorageFetchStrategy::default(),
chain_id: None,
block_context_requirements: BlockContextRequirements::lenient(),
max_concurrent_proofs: DEFAULT_MAX_CONCURRENT_PROOFS,
}
}
pub fn max_concurrent_proofs(mut self, cap: usize) -> Self {
self.max_concurrent_proofs = cap.max(1);
self
}
pub fn block(mut self, block: BlockId) -> Self {
self.block = block;
self
}
pub fn latest_block(mut self) -> Self {
self.block = BlockId::latest();
self
}
pub fn spec(mut self, spec_id: SpecId) -> Self {
self.spec_id = spec_id;
self
}
pub fn chain_id(mut self, chain_id: u64) -> Self {
self.chain_id = Some(chain_id);
self
}
pub fn cache_config(mut self, cache_config: CacheConfig) -> Self {
self.cache_config = Some(cache_config);
self
}
pub fn shared_memory_capacity(mut self, capacity: SharedMemoryCapacity) -> Self {
self.shared_memory_capacity = capacity;
self
}
pub fn storage_batch_config(mut self, config: impl Into<StorageBatchConfig>) -> Self {
self.storage_batch_config = config.into().normalized();
self
}
pub fn speed_mode(self, mode: CacheSpeedMode) -> Self {
self.storage_batch_config(mode)
}
pub fn storage_fetch_strategy(mut self, strategy: StorageFetchStrategy) -> Self {
self.storage_fetch_strategy = strategy;
self
}
pub fn bulk_call_config(self, config: crate::bulk_storage::BulkCallConfig) -> Self {
self.storage_fetch_strategy(StorageFetchStrategy::BulkCall(config))
}
pub fn block_context_requirements(mut self, reqs: BlockContextRequirements) -> Self {
self.block_context_requirements = reqs;
self
}
pub fn strict_block_context(mut self, strict: bool) -> Self {
self.block_context_requirements = if strict {
BlockContextRequirements::strict()
} else {
BlockContextRequirements::lenient()
};
self
}
pub async fn build(self) -> EvmCache {
let explicit_chain_id = self.chain_id;
let provider = self.provider.clone();
let strategy = self.storage_fetch_strategy;
let storage_batch_config = self.storage_batch_config;
let mut cache = EvmCache::with_cache_capacity_and_storage_batch_config(
self.provider,
self.block,
self.cache_config,
self.spec_id,
self.shared_memory_capacity,
self.storage_batch_config,
self.max_concurrent_proofs,
)
.await;
if let Some(chain_id) = explicit_chain_id {
cache.set_chain_id(chain_id);
}
apply_storage_fetch_strategy(&mut cache, provider, strategy, storage_batch_config);
cache
}
pub async fn try_build(self) -> Result<EvmCache, BlockContextError> {
let explicit_chain_id = self.chain_id;
let reqs = self.block_context_requirements;
let block = self.block;
let provider = self.provider.clone();
let strategy = self.storage_fetch_strategy;
let storage_batch_config = self.storage_batch_config;
let mut cache = EvmCache::with_cache_capacity_and_storage_batch_config(
self.provider,
self.block,
self.cache_config,
self.spec_id,
self.shared_memory_capacity,
self.storage_batch_config,
self.max_concurrent_proofs,
)
.await;
if let Some(chain_id) = explicit_chain_id {
cache.set_chain_id(chain_id);
}
cache.set_block_context_requirements(reqs);
apply_storage_fetch_strategy(&mut cache, provider.clone(), strategy, storage_batch_config);
if reqs != BlockContextRequirements::lenient() {
match provider.get_block(block).await {
Ok(Some(blk)) => reqs.validate_header(blk.header())?,
Ok(None) => {
return Err(BlockContextError::FetchFailed(format!(
"no block header returned for {block:?}"
)));
}
Err(e) => return Err(BlockContextError::FetchFailed(e.to_string())),
}
}
Ok(cache)
}
}
fn apply_storage_fetch_strategy<P>(
cache: &mut EvmCache,
provider: Arc<P>,
strategy: StorageFetchStrategy,
batch_config: StorageBatchConfig,
) where
P: Provider<AnyNetwork> + 'static,
{
match strategy {
StorageFetchStrategy::BulkCall(config)
if config == crate::bulk_storage::BulkCallConfig::default() => {}
StorageFetchStrategy::BulkCall(config) => {
let fallback = point_read_storage_fetcher(provider.clone(), batch_config);
cache.set_storage_batch_fetcher(
crate::bulk_storage::bulk_call_storage_fetcher_with_fallback(
provider, config, fallback,
),
);
}
StorageFetchStrategy::PointRead => {
cache.set_storage_batch_fetcher(point_read_storage_fetcher(provider, batch_config));
}
}
}
type CacheEvm<'a> = revm::MainnetEvm<
Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
>;
type InspectorCacheEvm<'a, INSP> = revm::MainnetEvm<
Context<BlockEnv, TxEnv, CfgEnv, &'a mut ForkCacheDB, Journal<&'a mut ForkCacheDB>, ()>,
INSP,
>;
const DEFAULT_SHARED_MEMORY_CAPACITY: usize = 64 * 1024;
const DEFAULT_MAX_CONCURRENT_PROOFS: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SharedMemoryCapacity {
Fixed(usize),
Auto,
}
impl Default for SharedMemoryCapacity {
fn default() -> Self {
Self::Fixed(DEFAULT_SHARED_MEMORY_CAPACITY)
}
}
impl SharedMemoryCapacity {
pub const MIN_AUTO: usize = DEFAULT_SHARED_MEMORY_CAPACITY;
pub const MAX_AUTO: usize = 4 * 1024 * 1024;
const AUTO_BYTES_PER_SLOT: usize = 16;
pub(crate) fn resolve(self, loaded_slots: usize) -> usize {
match self {
Self::Fixed(bytes) => bytes,
Self::Auto => loaded_slots
.saturating_mul(Self::AUTO_BYTES_PER_SLOT)
.clamp(Self::MIN_AUTO, Self::MAX_AUTO),
}
}
}
pub struct EvmCache {
backend: SharedBackend,
blockchain_db: BlockchainDb,
db: ForkCacheDB,
token_decimals: HashMap<Address, u8>,
block: BlockId,
cache_config: Option<CacheConfig>,
immutable_cache: ImmutableDataCache,
timestamp_override: Option<u64>,
chain_id: u64,
block_number: Option<u64>,
basefee: Option<u64>,
coinbase: Option<Address>,
prevrandao: Option<B256>,
block_gas_limit: Option<u64>,
block_context_requirements: BlockContextRequirements,
storage_batch_config: StorageBatchConfig,
shared_memory_buffer: Rc<RefCell<Vec<u8>>>,
rpc_caller: Option<RpcCallFn>,
snapshot_generation: u64,
storage_batch_fetcher: Option<StorageBatchFetchFn>,
account_proof_fetcher: Option<AccountProofFetchFn>,
block_state_diff_fetcher: Option<BlockStateDiffFetchFn>,
account_fields_fetcher: Option<AccountFieldsFetchFn>,
code_seeds: HashMap<Address, CodeSeedState>,
erc20_balance_slots: HashMap<Address, TrackedMapping>,
spec_id: SpecId,
base: Option<Arc<snapshot::BaseState>>,
base_dirty: HashSet<Address>,
base_full_rebuild: bool,
base_storage_lens: HashMap<Address, usize>,
shared_memory_capacity: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SimStatus {
Success,
Revert,
Halt {
reason: String,
},
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct CallSimulationResult {
pub status: SimStatus,
pub gas_used: u64,
pub token_deltas: HashMap<Address, I256>,
pub logs: Vec<Log>,
pub access_list: AccessList,
pub output: Bytes,
}
sol!(
#[sol(rpc)]
contract IERC20 {
function balanceOf(address target) returns (uint256);
function decimals() returns (uint8);
function allowance(address owner, address spender) returns (uint256);
}
);
pub fn parse_evm_spec(spec: &str) -> SpecId {
let mut chars = spec.chars();
let title_case: String = match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + &chars.as_str().to_lowercase(),
None => String::new(),
};
title_case.parse::<SpecId>().unwrap_or_else(|_| {
warn!(spec, "Unknown EVM spec, defaulting to Cancun");
SpecId::CANCUN
})
}
impl EvmCache {
pub fn builder<P>(provider: Arc<P>) -> EvmCacheBuilder<P>
where
P: Provider<AnyNetwork> + 'static,
{
EvmCacheBuilder::new(provider)
}
pub async fn new<P>(provider: Arc<P>) -> Self
where
P: Provider<AnyNetwork> + 'static,
{
Self::at_block(provider, BlockId::latest()).await
}
pub async fn at_block<P>(provider: Arc<P>, block: BlockId) -> Self
where
P: Provider<AnyNetwork> + 'static,
{
Self::with_cache(provider, block, None, SpecId::CANCUN).await
}
pub async fn with_cache<P>(
provider: Arc<P>,
block: BlockId,
cache_config: Option<CacheConfig>,
spec_id: SpecId,
) -> Self
where
P: Provider<AnyNetwork> + 'static,
{
Self::with_cache_capacity(
provider,
block,
cache_config,
spec_id,
SharedMemoryCapacity::default(),
)
.await
}
pub async fn with_cache_capacity<P>(
provider: Arc<P>,
block: BlockId,
cache_config: Option<CacheConfig>,
spec_id: SpecId,
shared_memory_capacity: SharedMemoryCapacity,
) -> Self
where
P: Provider<AnyNetwork> + 'static,
{
Self::with_cache_capacity_and_storage_batch_config(
provider,
block,
cache_config,
spec_id,
shared_memory_capacity,
StorageBatchConfig::default(),
DEFAULT_MAX_CONCURRENT_PROOFS,
)
.await
}
#[allow(clippy::too_many_arguments)]
async fn with_cache_capacity_and_storage_batch_config<P>(
provider: Arc<P>,
block: BlockId,
cache_config: Option<CacheConfig>,
spec_id: SpecId,
shared_memory_capacity: SharedMemoryCapacity,
storage_batch_config: StorageBatchConfig,
max_concurrent_proofs: usize,
) -> Self
where
P: Provider<AnyNetwork> + 'static,
{
let block_id = block;
let storage_batch_config = storage_batch_config.normalized();
let max_concurrent_proofs = max_concurrent_proofs.max(1);
let (block_number, basefee, coinbase, prevrandao, block_gas_limit) =
match provider.get_block(block_id).await {
Ok(Some(blk)) => {
let h = blk.header();
(
Some(h.number()),
h.base_fee_per_gas(),
Some(h.beneficiary()),
h.mix_hash(),
Some(h.gas_limit()),
)
}
Ok(None) => {
debug!("Block header not found for block context initialization");
(None, None, None, None, None)
}
Err(e) => {
debug!(error = %e, "Failed to fetch block header for block context");
(None, None, None, None, None)
}
};
if let Some(cfg) = &cache_config {
let _ = fs::create_dir_all(cfg.chain_dir());
}
let blockchain_db = if let Some(cfg) = &cache_config {
let binary_path = cfg.binary_state_cache_path();
if binary_path.exists() {
let meta = BlockchainDbMeta::default();
let db = BlockchainDb::new(meta, None);
if binary_state::load_binary_state(&db, &binary_path) {
db
} else {
let meta = BlockchainDbMeta::default();
BlockchainDb::new(meta, None)
}
} else {
let meta = BlockchainDbMeta::default();
BlockchainDb::new(meta, None)
}
} else {
let meta = BlockchainDbMeta::default();
BlockchainDb::new(meta, None)
};
if let Some(cfg) = &cache_config {
let has_filter = !cfg.maintain_addresses.is_empty() || !cfg.maintain_slots.is_empty();
if has_filter {
let mut storage = blockchain_db.storage().write();
let before_contracts = storage.len();
let before_slots: usize = storage.values().map(|s| s.len()).sum();
let addrs_to_remove: Vec<Address> = storage
.keys()
.filter(|addr| {
!cfg.maintain_addresses.contains(*addr)
&& !cfg.maintain_slots.contains_key(*addr)
})
.copied()
.collect();
for addr in &addrs_to_remove {
storage.remove(addr);
}
for (addr, allowed_slots) in &cfg.maintain_slots {
if let Some(addr_storage) = storage.get_mut(addr) {
addr_storage.retain(|slot, _| allowed_slots.contains(slot));
}
}
let after_contracts = storage.len();
let after_slots: usize = storage.values().map(|s| s.len()).sum();
drop(storage);
debug!(
contracts_removed = before_contracts.saturating_sub(after_contracts),
slots_removed = before_slots.saturating_sub(after_slots),
contracts_kept = after_contracts,
slots_kept = after_slots,
"Filtered cached storage by maintain list"
);
}
}
if let Some(cfg) = &cache_config {
let bytecode_path = cfg.bytecode_cache_path();
if let Some(bytecode_cache) = BytecodeCache::load(&bytecode_path) {
let loaded_count = Self::seed_bytecodes_from_cache(&blockchain_db, &bytecode_cache);
if loaded_count > 0 {
debug!(
count = loaded_count,
path = ?bytecode_path,
"Loaded contract bytecodes from cache"
);
}
}
}
let code_seeds: HashMap<Address, CodeSeedState> = cache_config
.as_ref()
.and_then(|cfg| CodeSeedCache::load(&cfg.code_seeds_cache_path()))
.map(|cache| {
let accounts = blockchain_db.accounts().read();
let before = cache.entries.len();
let mut entries = cache.entries;
entries.retain(|addr, state| {
accounts.get(addr).is_some_and(|info| {
info.code.as_ref().is_some_and(|code| !code.is_empty())
&& info.code_hash == state.code_hash()
})
});
if entries.len() < before {
debug!(
pruned = before - entries.len(),
kept = entries.len(),
"Pruned code-seed marks whose code did not survive the reload"
);
}
entries
})
.unwrap_or_default();
let immutable_cache = cache_config
.as_ref()
.and_then(|cfg| {
let path = cfg.immutable_cache_path();
ImmutableDataCache::load(&path).inspect(|cache| {
debug!(
token_decimals = cache.token_decimals.len(),
path = ?path,
"Loaded immutable data from cache"
);
})
})
.unwrap_or_default();
let token_decimals = immutable_cache.token_decimals.clone();
let provider_for_rpc = provider.clone();
let rpc_caller: RpcCallFn = Arc::new(move |to: Address, calldata: Bytes| {
let handle = block_in_place_handle()?;
tokio::task::block_in_place(|| {
handle.block_on(async {
let tx = TransactionRequest::default()
.to(to)
.input(alloy_primitives::Bytes::from(calldata.to_vec()).into());
provider_for_rpc
.call(tx.into())
.await
.map_err(|e| RpcError::provider("eth_call", e))
})
})
});
let storage_batch_fetcher: StorageBatchFetchFn =
crate::bulk_storage::bulk_call_storage_fetcher_with_fallback(
provider.clone(),
crate::bulk_storage::BulkCallConfig::default(),
point_read_storage_fetcher(provider.clone(), storage_batch_config),
);
let provider_for_proof = provider.clone();
let account_proof_fetcher: AccountProofFetchFn = Arc::new(
move |requests: Vec<(Address, Vec<U256>)>, current_block: BlockId| {
let handle = match block_in_place_handle() {
Ok(handle) => handle,
Err(e) => {
return requests
.into_iter()
.map(|(addr, _keys)| {
(
addr,
Err(StorageFetchError::Runtime(RuntimeError::MissingRuntime {
details: e.to_string(),
})),
)
})
.collect();
}
};
let provider = provider_for_proof.clone();
tokio::task::block_in_place(|| {
handle.block_on(async {
use futures::StreamExt;
futures::stream::iter(requests.into_iter().map(|(addr, keys)| {
let provider = provider.clone();
async move {
let proof_keys: Vec<B256> =
keys.iter().map(|slot| B256::from(*slot)).collect();
let outcome = provider
.get_proof(addr, proof_keys)
.block_id(current_block)
.await;
match outcome {
Ok(response) => {
let slots = response
.storage_proof
.iter()
.map(|proof| {
(
U256::from_be_bytes(proof.key.as_b256().0),
proof.value,
)
})
.collect();
(
addr,
Ok(AccountProof {
storage_hash: response.storage_hash,
balance: response.balance,
nonce: response.nonce,
code_hash: response.code_hash,
slots,
}),
)
}
Err(e) => {
(addr, Err(StorageFetchError::provider("eth_getProof", e)))
}
}
}
}))
.buffered(max_concurrent_proofs)
.collect::<Vec<_>>()
.await
})
})
},
);
let provider_for_fields = provider.clone();
let account_fields_fetcher: AccountFieldsFetchFn =
Arc::new(move |addresses: Vec<Address>, block: BlockId| {
let handle = block_in_place_handle()?;
tokio::task::block_in_place(|| {
handle.block_on(crate::bulk_storage::fetch_account_fields_bulk(
provider_for_fields.as_ref(),
&addresses,
block,
))
})
});
let provider_for_trace = provider.clone();
let block_state_diff_fetcher: BlockStateDiffFetchFn = Arc::new(move |block: BlockId| {
let handle = block_in_place_handle()?;
tokio::task::block_in_place(|| {
handle.block_on(async {
let (method, params) = trace_rpc_method_and_params(block);
let response = provider_for_trace
.client()
.request::<_, serde_json::Value>(method, params)
.await
.map_err(|e| StorageFetchError::provider(method, e))?;
parse_block_state_diff_trace(&response)
.map_err(|err| StorageFetchError::custom(err.to_string()))
})
})
});
let chain_id = match cache_config.as_ref() {
Some(cfg) => cfg.chain_id,
None => match provider.get_chain_id().await {
Ok(id) => id,
Err(e) => {
debug!(
error = %e,
"Failed to infer chain ID from provider; defaulting to 1 (Ethereum mainnet). Set it explicitly via EvmCacheBuilder::chain_id."
);
1
}
},
};
let backend =
SharedBackend::spawn_backend(provider, blockchain_db.clone(), Some(block_id)).await;
let db = CacheDB::new(backend.clone());
let loaded_slots = match shared_memory_capacity {
SharedMemoryCapacity::Auto => blockchain_db
.storage()
.read()
.values()
.map(|s| s.len())
.sum(),
SharedMemoryCapacity::Fixed(_) => 0,
};
let shared_memory_capacity = shared_memory_capacity.resolve(loaded_slots);
Self {
backend,
blockchain_db,
db,
token_decimals,
block,
cache_config,
immutable_cache,
timestamp_override: None,
chain_id,
block_number,
basefee,
coinbase,
prevrandao,
block_gas_limit,
block_context_requirements: BlockContextRequirements::lenient(),
storage_batch_config,
shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(shared_memory_capacity))),
snapshot_generation: 0,
rpc_caller: Some(rpc_caller),
storage_batch_fetcher: Some(storage_batch_fetcher),
account_proof_fetcher: Some(account_proof_fetcher),
block_state_diff_fetcher: Some(block_state_diff_fetcher),
account_fields_fetcher: Some(account_fields_fetcher),
code_seeds,
erc20_balance_slots: HashMap::new(),
spec_id,
base: None,
base_dirty: HashSet::new(),
base_full_rebuild: false,
base_storage_lens: HashMap::new(),
shared_memory_capacity,
}
}
fn seed_bytecodes_from_cache(db: &BlockchainDb, cache: &BytecodeCache) -> usize {
let mut count = 0;
for (addr, entry) in &cache.contracts {
if entry.bytecode.is_empty() {
continue;
}
let bytecode = Bytecode::new_raw(Bytes::from(entry.bytecode.clone()));
let code_hash: B256 = bytecode.hash_slow();
let info = AccountInfo {
balance: U256::ZERO,
nonce: 0,
code_hash,
code: Some(bytecode),
account_id: None,
};
db.db().do_insert_account(*addr, info);
count += 1;
}
count
}
pub fn from_backend(
backend: SharedBackend,
blockchain_db: BlockchainDb,
block: BlockId,
chain_id: u64,
block_number: Option<u64>,
basefee: Option<u64>,
spec_id: SpecId,
) -> Self {
let db = CacheDB::new(backend.clone());
Self {
backend,
blockchain_db,
db,
token_decimals: HashMap::new(),
block,
cache_config: None,
immutable_cache: ImmutableDataCache::default(),
timestamp_override: None,
chain_id,
block_number,
basefee,
coinbase: None,
prevrandao: None,
block_gas_limit: None,
block_context_requirements: BlockContextRequirements::lenient(),
storage_batch_config: StorageBatchConfig::default(),
snapshot_generation: 0,
shared_memory_buffer: Rc::new(RefCell::new(Vec::with_capacity(
DEFAULT_SHARED_MEMORY_CAPACITY,
))),
rpc_caller: None,
storage_batch_fetcher: None,
account_proof_fetcher: None,
block_state_diff_fetcher: None,
account_fields_fetcher: None,
code_seeds: HashMap::new(),
erc20_balance_slots: HashMap::new(),
spec_id,
base: None,
base_dirty: HashSet::new(),
base_full_rebuild: false,
base_storage_lens: HashMap::new(),
shared_memory_capacity: DEFAULT_SHARED_MEMORY_CAPACITY,
}
}
pub fn flush(&self) -> Result<()> {
if let Some(cfg) = &self.cache_config {
let binary_path = cfg.binary_state_cache_path();
binary_state::save_binary_state(&self.blockchain_db, &binary_path)?;
let code_seeds_path = cfg.code_seeds_cache_path();
CodeSeedCache {
entries: self.code_seeds.clone(),
}
.save(&code_seeds_path)?;
debug!(
count = self.code_seeds.len(),
path = ?code_seeds_path,
"Updated code-seed mark cache (binary format)"
);
let bytecode_path = cfg.bytecode_cache_path();
let mut bytecode_cache = BytecodeCache::load(&bytecode_path).unwrap_or_default();
bytecode_cache.merge_from_db(&self.blockchain_db);
bytecode_cache.save(&bytecode_path)?;
debug!(
count = bytecode_cache.contracts.len(),
path = ?bytecode_path,
"Updated bytecode cache (binary format)"
);
let immutable_path = cfg.immutable_cache_path();
self.immutable_cache.save(&immutable_path)?;
debug!(
token_decimals = self.immutable_cache.token_decimals.len(),
path = ?immutable_path,
"Updated immutable data cache"
);
}
Ok(())
}
pub fn cache_config(&self) -> Option<&CacheConfig> {
self.cache_config.as_ref()
}
pub fn with_blockchain_db_mut<R>(&mut self, f: impl FnOnce(&BlockchainDb) -> R) -> R {
let result = f(&self.blockchain_db);
self.invalidate_base();
result
}
pub fn unchecked_blockchain_db(&self) -> &BlockchainDb {
&self.blockchain_db
}
pub fn unchecked_backend(&self) -> &SharedBackend {
&self.backend
}
pub fn db_mut(&mut self) -> &mut ForkCacheDB {
&mut self.db
}
pub fn rpc_call(&self, to: Address, calldata: Bytes) -> Option<Result<Bytes, RpcError>> {
self.rpc_caller
.as_ref()
.map(|caller| (caller)(to, calldata))
}
pub fn storage_batch_fetcher(&self) -> Option<&StorageBatchFetchFn> {
self.storage_batch_fetcher.as_ref()
}
pub fn account_proof_fetcher(&self) -> Option<&AccountProofFetchFn> {
self.account_proof_fetcher.as_ref()
}
pub fn block_state_diff_fetcher(&self) -> Option<&BlockStateDiffFetchFn> {
self.block_state_diff_fetcher.as_ref()
}
pub fn inject_storage_batch(&mut self, results: &[(Address, U256, U256)]) {
{
let mut storage = self.blockchain_db.storage().write();
for &(addr, slot, value) in results {
storage.entry(addr).or_default().insert(slot, value);
}
}
for &(addr, _, _) in results {
self.mark_base_dirty(addr);
}
}
pub fn inject_storage_batch_fresh(&mut self, results: &[(Address, U256, U256)]) {
let updates: Vec<StateUpdate> = results
.iter()
.map(|&(addr, slot, value)| StateUpdate::slot(addr, slot, value))
.collect();
let _ = self.apply_updates(&updates);
}
pub fn prewarm_slots(&mut self, requests: &[(Address, U256)]) -> PrewarmReport {
let Some(fetcher) = self.storage_batch_fetcher.clone() else {
return PrewarmReport {
loaded: 0,
failed: requests
.iter()
.map(|&(addr, slot)| {
(
addr,
slot,
StorageFetchError::custom("no storage batch fetcher installed"),
)
})
.collect(),
};
};
let results = fetcher(requests.to_vec(), self.block);
let mut to_inject = Vec::with_capacity(results.len());
let mut failed = Vec::new();
for (addr, slot, result) in results {
match result {
Ok(value) => to_inject.push((addr, slot, value)),
Err(e) => failed.push((addr, slot, e)),
}
}
self.inject_storage_batch(&to_inject);
PrewarmReport {
loaded: to_inject.len(),
failed,
}
}
pub fn apply_update(&mut self, update: &StateUpdate) -> StateDiff {
self.bump_snapshot_generation();
let mut diff = StateDiff::default();
match update {
StateUpdate::Slot {
address,
slot,
value,
} => {
if let Some(change) = self.apply_slot(*address, *slot, *value) {
diff.slots.push(change);
}
}
StateUpdate::SlotDelta {
address,
slot,
delta,
} => match self.cached_storage_value(*address, *slot) {
Some(current) => {
let new = delta.apply(current);
self.write_slot_through(*address, *slot, new);
if current != new {
diff.slots.push(SlotChange {
address: *address,
slot: *slot,
old: current,
new,
});
}
}
None => diff.skipped.push(SkippedDelta {
address: *address,
slot: *slot,
delta: *delta,
}),
},
StateUpdate::SlotMasked {
address,
slot,
mask,
value,
} => match self.cached_storage_value(*address, *slot) {
Some(old) => {
let new = (old & !*mask) | (*value & *mask);
self.write_slot_through(*address, *slot, new);
if old != new {
diff.slots.push(SlotChange {
address: *address,
slot: *slot,
old,
new,
});
}
}
None => diff.skipped_masks.push(SkippedMask {
address: *address,
slot: *slot,
mask: *mask,
value: *value,
}),
},
StateUpdate::BalanceDelta { address, delta } => {
match self.apply_balance_delta(*address, *delta) {
Ok(Some(change)) => diff.accounts.push(change),
Ok(None) => {}
Err(skipped) => diff.skipped_balances.push(skipped),
}
}
StateUpdate::Account { address, patch } => {
match self.apply_account_patch(*address, patch, false) {
Ok(Some(change)) => diff.accounts.push(change),
Ok(None) => {}
Err(skipped) => diff.skipped_accounts.push(skipped),
}
}
StateUpdate::AccountUpsert { address, patch } => {
if let Some(change) = self
.apply_account_patch(*address, patch, true)
.expect("AccountUpsert never skips cold account patches")
{
diff.accounts.push(change);
}
}
StateUpdate::Purge { address, scope } => {
diff.purged.push(self.apply_purge(*address, scope));
}
}
diff
}
pub fn apply_updates(&mut self, updates: &[StateUpdate]) -> StateDiff {
if !updates.is_empty() {
self.bump_snapshot_generation();
}
let mut diff = StateDiff::default();
let mut i = 0;
while i < updates.len() {
match &updates[i] {
StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. } => {
let run_end = updates[i..]
.iter()
.position(|u| {
!matches!(u, StateUpdate::Slot { .. } | StateUpdate::SlotDelta { .. })
})
.map(|off| i + off)
.unwrap_or(updates.len());
self.apply_slot_run(&updates[i..run_end], &mut diff);
i = run_end;
}
_ => {
diff.merge(self.apply_update(&updates[i]));
i += 1;
}
}
}
diff
}
fn apply_slot_run(&mut self, run: &[StateUpdate], diff: &mut StateDiff) {
let mut dirtied: Vec<Address> = Vec::new();
let overlay = &mut self.db.cache.accounts;
let mut storage = self.blockchain_db.storage().write();
for update in run {
let (address, slot, old, new) = match update {
StateUpdate::Slot {
address,
slot,
value,
} => {
let old = read_slot_account_state_aware(overlay, &storage, *address, *slot)
.unwrap_or(U256::ZERO);
(*address, *slot, old, *value)
}
StateUpdate::SlotDelta {
address,
slot,
delta,
} => match read_slot_account_state_aware(overlay, &storage, *address, *slot) {
Some(current) => (*address, *slot, current, delta.apply(current)),
None => {
diff.skipped.push(SkippedDelta {
address: *address,
slot: *slot,
delta: *delta,
});
continue;
}
},
_ => unreachable!("apply_slot_run only processes Slot/SlotDelta"),
};
write_slot_into(overlay, &mut storage, address, slot, new);
dirtied.push(address);
if old != new {
diff.slots.push(SlotChange {
address,
slot,
old,
new,
});
}
}
drop(storage);
for address in dirtied {
self.mark_base_dirty(address);
}
}
fn apply_slot(&mut self, address: Address, slot: U256, value: U256) -> Option<SlotChange> {
let old = self
.cached_storage_value(address, slot)
.unwrap_or(U256::ZERO);
self.write_slot_through(address, slot, value);
(old != value).then_some(SlotChange {
address,
slot,
old,
new: value,
})
}
fn write_slot_through(&mut self, address: Address, slot: U256, value: U256) {
{
let mut storage = self.blockchain_db.storage().write();
storage.entry(address).or_default().insert(slot, value);
}
if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
db_account.storage.insert(slot, value);
}
self.mark_base_dirty(address);
}
pub fn modify_slot(
&mut self,
address: Address,
slot: U256,
f: impl FnOnce(Option<U256>) -> Option<U256>,
) -> Option<SlotChange> {
let current = self.cached_storage_value(address, slot);
let new = f(current)?;
self.bump_snapshot_generation();
self.write_slot_through(address, slot, new);
let old = current.unwrap_or(U256::ZERO);
(old != new).then_some(SlotChange {
address,
slot,
old,
new,
})
}
pub fn modify_account_balance(
&mut self,
address: Address,
f: impl FnOnce(Option<U256>) -> Option<U256>,
) -> Option<AccountChange> {
let base = self.loaded_account_info(address);
let current_balance = base.as_ref().map(|info| info.balance);
let new_balance = f(current_balance)?;
let mut info = base.unwrap_or_default();
let old_balance = info.balance;
info.balance = new_balance;
self.write_account_info_through(address, info);
(old_balance != new_balance).then_some(AccountChange {
address,
balance: Some((old_balance, new_balance)),
nonce: None,
code_hash: None,
})
}
fn apply_balance_delta(
&mut self,
address: Address,
delta: SlotDelta,
) -> std::result::Result<Option<AccountChange>, SkippedBalanceDelta> {
let Some(mut info) = self.loaded_account_info(address) else {
return Err(SkippedBalanceDelta { address, delta });
};
let old_balance = info.balance;
let new_balance = delta.apply(old_balance);
info.balance = new_balance;
self.write_account_info_through(address, info);
Ok((old_balance != new_balance).then_some(AccountChange {
address,
balance: Some((old_balance, new_balance)),
nonce: None,
code_hash: None,
}))
}
fn loaded_account_info(&self, address: Address) -> Option<AccountInfo> {
let mut info = if let Some(a) = self.db.cache.accounts.get(&address) {
if matches!(a.account_state, AccountState::NotExisting) {
return None;
}
a.info.clone()
} else {
self.blockchain_db
.accounts()
.read()
.get(&address)
.cloned()?
};
if info.code_hash == B256::ZERO {
info.code_hash = revm::primitives::KECCAK_EMPTY;
}
Some(info)
}
fn write_account_info_through(&mut self, address: Address, mut info: AccountInfo) {
if info.code_hash == B256::ZERO {
info.code_hash = revm::primitives::KECCAK_EMPTY;
}
let overlay_present = self.db.cache.accounts.contains_key(&address);
{
let mut accounts = self.blockchain_db.accounts().write();
accounts.insert(address, info.clone());
}
if overlay_present {
self.db.insert_account_info(address, info);
}
self.mark_base_dirty(address);
}
fn apply_account_patch(
&mut self,
address: Address,
patch: &AccountPatch,
allow_cold_upsert: bool,
) -> std::result::Result<Option<AccountChange>, SkippedAccountPatch> {
let mut info = match self.loaded_account_info(address) {
Some(info) => info,
None if account_patch_is_empty(patch) => return Ok(None),
None if allow_cold_upsert => AccountInfo::default(),
None => {
return Err(SkippedAccountPatch {
address,
patch: patch.clone(),
});
}
};
let old_balance = info.balance;
let old_nonce = info.nonce;
let old_code_hash = info.code_hash;
if let Some(balance) = patch.balance {
info.balance = balance;
}
if let Some(nonce) = patch.nonce {
info.nonce = nonce;
}
if let Some(code) = &patch.code {
let bytecode = Bytecode::new_raw(code.clone());
info.code_hash = bytecode.hash_slow();
info.code = Some(bytecode);
}
let change = AccountChange {
address,
balance: (old_balance != info.balance).then_some((old_balance, info.balance)),
nonce: (old_nonce != info.nonce).then_some((old_nonce, info.nonce)),
code_hash: (old_code_hash != info.code_hash).then_some((old_code_hash, info.code_hash)),
};
if change.balance.is_none() && change.nonce.is_none() && change.code_hash.is_none() {
return Ok(None);
}
self.write_account_info_through(address, info);
Ok(Some(change))
}
fn apply_purge(&mut self, address: Address, scope: &PurgeScope) -> PurgeRecord {
match scope {
PurgeScope::Account => {
let (slots_removed, account_removed) = self.purge_account_inner(address);
PurgeRecord {
address,
scope: PurgeScope::Account,
slots_removed,
account_removed,
}
}
PurgeScope::AllStorage => {
let slots_removed = self.purge_contract_storage_inner(address);
PurgeRecord {
address,
scope: PurgeScope::AllStorage,
slots_removed,
account_removed: false,
}
}
PurgeScope::Slots(slots) => {
let slots_removed = self.purge_contract_slots_inner(address, slots);
PurgeRecord {
address,
scope: PurgeScope::Slots(slots.clone()),
slots_removed,
account_removed: false,
}
}
}
}
pub fn set_storage_batch_fetcher(&mut self, f: StorageBatchFetchFn) {
self.storage_batch_fetcher = Some(f);
}
pub fn set_account_proof_fetcher(&mut self, f: AccountProofFetchFn) {
self.account_proof_fetcher = Some(f);
}
pub fn set_block_state_diff_fetcher(&mut self, f: BlockStateDiffFetchFn) {
self.block_state_diff_fetcher = Some(f);
}
pub fn set_account_fields_fetcher(&mut self, f: AccountFieldsFetchFn) {
self.account_fields_fetcher = Some(f);
}
pub fn account_fields_fetcher(&self) -> Option<&AccountFieldsFetchFn> {
self.account_fields_fetcher.as_ref()
}
pub fn cached_storage_value(&self, address: Address, slot: U256) -> Option<U256> {
if let Some(db_account) = self.db.cache.accounts.get(&address) {
if let Some(value) = db_account.storage.get(&slot) {
return Some(*value);
}
if matches!(
db_account.account_state,
AccountState::StorageCleared | AccountState::NotExisting
) {
return Some(U256::ZERO);
}
}
let storage = self.blockchain_db.storage().read();
storage.get(&address).and_then(|s| s.get(&slot).copied())
}
pub fn verify_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
Ok(self.verify_slots_inner(slots)?.0)
}
fn verify_slots_inner(
&mut self,
slots: &[(Address, U256)],
) -> Result<(Vec<SlotChange>, usize)> {
let (changed, outcomes) = self.verify_slots_core(slots)?;
let fetched_ok = outcomes
.iter()
.filter(|o| matches!(o.fetch, SlotFetch::Value(_) | SlotFetch::Zero))
.count();
Ok((changed, fetched_ok))
}
pub(crate) fn classify(fetched: StorageFetchResult<U256>) -> SlotFetch {
match fetched {
Ok(v) if v != U256::ZERO => SlotFetch::Value(v),
Ok(_) => SlotFetch::Zero,
Err(e) => SlotFetch::FetchFailed {
reason: e.to_string(),
},
}
}
fn verify_slots_core(
&mut self,
slots: &[(Address, U256)],
) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
if slots.is_empty() {
return Ok((Vec::new(), Vec::new()));
}
let fetcher = self
.storage_batch_fetcher
.as_ref()
.ok_or(CacheError::MissingStorageBatchFetcher)?
.clone();
let cached: HashMap<(Address, U256), Option<U256>> = slots
.iter()
.map(|&(addr, slot)| ((addr, slot), self.cached_storage_value(addr, slot)))
.collect();
let results = (fetcher)(slots.to_vec(), self.block);
let mut changed = Vec::new();
let mut outcomes = Vec::with_capacity(results.len());
let mut to_inject = Vec::new();
for (addr, slot, fetched) in results {
let fetch = Self::classify(match &fetched {
Ok(v) => Ok(*v),
Err(e) => Err(StorageFetchError::custom(e.to_string())),
});
outcomes.push(SlotOutcome {
address: addr,
slot,
fetch,
});
let fresh = match fetched {
Ok(value) => value,
Err(e) => {
debug!(%addr, %slot, error = %e, "verify_slots: fetch failed, skipping slot");
continue;
}
};
let old = cached
.get(&(addr, slot))
.copied()
.flatten()
.unwrap_or(U256::ZERO);
if fresh != old {
to_inject.push((addr, slot, fresh));
changed.push(SlotChange {
address: addr,
slot,
old,
new: fresh,
});
}
}
if !to_inject.is_empty() {
self.inject_storage_batch_fresh(&to_inject);
}
Ok((changed, outcomes))
}
#[cfg(feature = "reactive")]
pub(crate) fn verify_slots_with_outcomes(
&mut self,
slots: &[(Address, U256)],
) -> Result<(Vec<SlotChange>, Vec<SlotOutcome>)> {
self.verify_slots_core(slots)
}
pub fn reconcile_slots(&mut self, slots: &[(Address, U256)]) -> Result<Vec<SlotChange>> {
let (changed, fetched_ok) = self.verify_slots_inner(slots)?;
if !slots.is_empty() && fetched_ok == 0 {
return Err(CacheError::ReconcileFetchFailed {
requested: slots.len(),
});
}
Ok(changed)
}
pub fn purge_account(&mut self, addr: Address) {
let _ = self.apply_update(&StateUpdate::purge(addr, PurgeScope::Account));
}
fn purge_account_inner(&mut self, addr: Address) -> (usize, bool) {
self.code_seeds.remove(&addr);
let overlay_removed = self.db.cache.accounts.remove(&addr).is_some();
let backend_account_removed = self
.blockchain_db
.accounts()
.write()
.remove(&addr)
.is_some();
let backend_storage_removed = self.blockchain_db.storage().write().remove(&addr);
let slots_removed = backend_storage_removed
.map(|slots| slots.len())
.unwrap_or(0);
let account_removed = overlay_removed || backend_account_removed;
if account_removed || slots_removed > 0 {
debug!(
account = %addr,
overlay_removed,
backend_account_removed,
backend_storage_slots = slots_removed,
"purged account from both cache layers"
);
}
self.mark_base_dirty(addr);
(slots_removed, account_removed)
}
pub fn chain_id(&self) -> u64 {
self.chain_id
}
pub fn set_chain_id(&mut self, chain_id: u64) {
self.chain_id = chain_id;
}
pub fn checkpoint(&self) -> revm::database::Cache {
self.db.cache.clone()
}
pub fn restore(&mut self, checkpoint: revm::database::Cache) {
self.db.cache = checkpoint;
}
pub fn session(&mut self) -> EvmSession<'_> {
EvmSession {
evm: self.build_evm(),
}
}
pub fn snapshot(&mut self) -> Arc<snapshot::EvmSnapshot> {
self.refresh_base();
let base = Arc::clone(self.base.as_ref().expect("refresh_base sets base"));
let mut overlay_accounts = HashMap::new();
let mut overlay_storage = HashMap::new();
let mut overlay_code_by_hash = HashMap::new();
let mut storage_cleared = std::collections::HashSet::new();
let mut accounts_not_existing = std::collections::HashSet::new();
for (addr, db_account) in &self.db.cache.accounts {
let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
let cleared =
not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
if not_existing {
accounts_not_existing.insert(*addr);
} else {
if let Some(code) = &db_account.info.code {
overlay_code_by_hash.insert(db_account.info.code_hash, code.clone());
}
overlay_accounts.insert(*addr, db_account.info.clone());
}
if cleared {
storage_cleared.insert(*addr);
let account_storage: HashMap<U256, U256> =
db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
overlay_storage.insert(*addr, account_storage);
} else if !db_account.storage.is_empty() {
let account_storage = overlay_storage.entry(*addr).or_default();
for (slot, value) in &db_account.storage {
account_storage.insert(*slot, *value);
}
}
}
Arc::new(snapshot::EvmSnapshot {
base,
overlay_accounts,
overlay_storage,
overlay_code_by_hash,
storage_cleared,
accounts_not_existing,
block_hashes: HashMap::new(),
block_number: self.block_number,
basefee: self.basefee,
coinbase: self.coinbase,
prevrandao: self.prevrandao,
gas_limit: self.block_gas_limit,
chain_id: self.chain_id,
timestamp: self.timestamp_override,
spec_id: self.spec_id,
shared_memory_capacity: self.shared_memory_capacity,
})
}
pub fn invalidate_snapshot_base(&mut self) {
self.invalidate_base();
}
fn refresh_base(&mut self) {
if self.base.is_none() || self.base_full_rebuild {
self.base = Some(Arc::new(self.build_base_full()));
self.base_dirty.clear();
self.base_full_rebuild = false;
return;
}
{
let db_storage = self.blockchain_db.storage().read();
for (addr, slots) in db_storage.iter() {
if self.base_storage_lens.get(addr).copied() != Some(slots.len()) {
self.base_dirty.insert(*addr);
}
}
let db_accounts = self.blockchain_db.accounts().read();
let base = self.base.as_ref().expect("base present in case 2/3/4");
for addr in db_accounts.keys() {
if !base.accounts.contains_key(addr) {
self.base_dirty.insert(*addr);
}
}
}
if self.base_dirty.is_empty() {
return;
}
let prev = self.base.as_ref().expect("base present in case 4");
let mut accounts = prev.accounts.clone();
let mut storage = prev.storage.clone();
let db_accounts = self.blockchain_db.accounts().read();
let db_storage = self.blockchain_db.storage().read();
for addr in self.base_dirty.iter().copied() {
match db_accounts.get(&addr) {
Some(info) => {
accounts.insert(addr, info.clone());
}
None => {
accounts.remove(&addr);
}
}
match db_storage.get(&addr) {
Some(slots) => {
let rebuilt: HashMap<U256, U256> =
slots.iter().map(|(k, v)| (*k, *v)).collect();
self.base_storage_lens.insert(addr, rebuilt.len());
storage.insert(addr, Arc::new(rebuilt));
}
None => {
storage.remove(&addr);
self.base_storage_lens.remove(&addr);
}
}
}
drop(db_accounts);
drop(db_storage);
let code_by_hash = Self::code_index(&accounts);
self.base = Some(Arc::new(snapshot::BaseState {
accounts,
storage,
code_by_hash,
}));
self.base_dirty.clear();
}
fn code_index(accounts: &HashMap<Address, AccountInfo>) -> HashMap<B256, Bytecode> {
accounts
.values()
.filter_map(|info| {
info.code
.as_ref()
.map(|code| (info.code_hash, code.clone()))
})
.collect()
}
fn build_base_full(&mut self) -> snapshot::BaseState {
let mut accounts = HashMap::new();
{
let db_accounts = self.blockchain_db.accounts().read();
for (addr, info) in db_accounts.iter() {
accounts.insert(*addr, info.clone());
}
}
let code_by_hash = Self::code_index(&accounts);
let mut storage = HashMap::new();
self.base_storage_lens.clear();
{
let db_storage = self.blockchain_db.storage().read();
for (addr, slots) in db_storage.iter() {
let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
self.base_storage_lens.insert(*addr, converted.len());
storage.insert(*addr, Arc::new(converted));
}
}
snapshot::BaseState {
accounts,
storage,
code_by_hash,
}
}
#[doc(hidden)]
pub fn snapshot_deep_clone(&self) -> Arc<snapshot::EvmSnapshot> {
let mut accounts = HashMap::new();
let mut storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
let mut code_by_hash = HashMap::new();
{
let db_accounts = self.blockchain_db.accounts().read();
for (addr, info) in db_accounts.iter() {
if let Some(code) = &info.code {
code_by_hash.insert(info.code_hash, code.clone());
}
accounts.insert(*addr, info.clone());
}
}
{
let db_storage = self.blockchain_db.storage().read();
for (addr, slots) in db_storage.iter() {
let converted: HashMap<U256, U256> = slots.iter().map(|(k, v)| (*k, *v)).collect();
storage.insert(*addr, converted);
}
}
let mut overlay_storage: HashMap<Address, HashMap<U256, U256>> = HashMap::new();
let mut storage_cleared = std::collections::HashSet::new();
let mut accounts_not_existing = std::collections::HashSet::new();
for (addr, db_account) in &self.db.cache.accounts {
let not_existing = matches!(db_account.account_state, AccountState::NotExisting);
let cleared =
not_existing || matches!(db_account.account_state, AccountState::StorageCleared);
if not_existing {
accounts_not_existing.insert(*addr);
accounts.remove(addr);
} else {
if let Some(code) = &db_account.info.code {
code_by_hash.insert(db_account.info.code_hash, code.clone());
}
accounts.insert(*addr, db_account.info.clone());
}
if cleared {
storage_cleared.insert(*addr);
storage.remove(addr);
let account_storage: HashMap<U256, U256> =
db_account.storage.iter().map(|(k, v)| (*k, *v)).collect();
overlay_storage.insert(*addr, account_storage);
} else {
let account_storage = storage.entry(*addr).or_default();
for (slot, value) in &db_account.storage {
account_storage.insert(*slot, *value);
}
}
}
let base = snapshot::BaseState {
accounts,
storage: storage
.into_iter()
.map(|(addr, slots)| (addr, Arc::new(slots)))
.collect(),
code_by_hash,
};
Arc::new(snapshot::EvmSnapshot {
base: Arc::new(base),
overlay_accounts: HashMap::new(),
overlay_storage,
overlay_code_by_hash: HashMap::new(),
storage_cleared,
accounts_not_existing,
block_hashes: HashMap::new(),
block_number: self.block_number,
basefee: self.basefee,
coinbase: self.coinbase,
prevrandao: self.prevrandao,
gas_limit: self.block_gas_limit,
chain_id: self.chain_id,
timestamp: self.timestamp_override,
spec_id: self.spec_id,
shared_memory_capacity: self.shared_memory_capacity,
})
}
fn mark_base_dirty(&mut self, address: Address) {
self.base_dirty.insert(address);
}
fn invalidate_base(&mut self) {
self.base_full_rebuild = true;
}
pub fn set_block(&mut self, block: BlockId) {
let changed = self.block != block;
let concrete_number = match block {
BlockId::Number(BlockNumberOrTag::Number(n)) => Some(n),
_ => None,
};
if changed {
self.block = block;
self.bump_snapshot_generation();
self.invalidate_base();
let _ = self.backend.set_pinned_block(block);
}
if changed || concrete_number.is_none() {
self.basefee = None;
}
self.block_number = concrete_number;
}
pub fn block(&self) -> BlockId {
self.block
}
pub fn snapshot_generation(&self) -> u64 {
self.snapshot_generation
}
fn bump_snapshot_generation(&mut self) {
self.snapshot_generation = self.snapshot_generation.wrapping_add(1);
}
pub fn set_timestamp(&mut self, timestamp: Option<u64>) {
self.timestamp_override = timestamp;
}
pub fn timestamp(&self) -> Option<u64> {
self.timestamp_override
}
pub fn block_number(&self) -> Option<u64> {
self.block_number
}
pub fn basefee(&self) -> Option<u64> {
self.basefee
}
pub fn set_block_context(&mut self, block_number: Option<u64>, basefee: Option<u64>) {
self.block_number = block_number;
self.basefee = basefee;
}
pub fn set_basefee(&mut self, basefee: U256) {
self.basefee = Some(basefee.saturating_to::<u64>());
}
pub fn set_coinbase(&mut self, coinbase: Option<Address>) {
self.coinbase = coinbase;
}
pub fn set_prevrandao(&mut self, prevrandao: Option<B256>) {
self.prevrandao = prevrandao;
}
pub fn set_block_gas_limit(&mut self, gas_limit: Option<u64>) {
self.block_gas_limit = gas_limit;
}
pub fn coinbase(&self) -> Option<Address> {
self.coinbase
}
pub fn prevrandao(&self) -> Option<B256> {
self.prevrandao
}
pub fn block_gas_limit(&self) -> Option<u64> {
self.block_gas_limit
}
pub fn set_block_context_requirements(&mut self, reqs: BlockContextRequirements) {
self.block_context_requirements = reqs;
}
pub fn advance_block<H: BlockHeader>(&mut self, header: &H) -> Result<(), BlockContextError> {
self.block_context_requirements.validate_header(header)?;
self.block_number = Some(header.number());
self.basefee = header.base_fee_per_gas();
self.coinbase = Some(header.beneficiary());
self.prevrandao = header.mix_hash();
self.block_gas_limit = Some(header.gas_limit());
self.timestamp_override = Some(header.timestamp());
let block = BlockId::number(header.number());
self.block = block;
let _ = self.backend.set_pinned_block(block);
self.bump_snapshot_generation();
Ok(())
}
pub fn repin_to_block(&mut self, block_number: u64) {
let old_block = self.block;
self.set_block(BlockId::Number(block_number.into()));
if let BlockId::Number(BlockNumberOrTag::Number(old_num)) = old_block {
let drift = block_number.saturating_sub(old_num);
if drift > 0 {
debug!(
old_block = old_num,
new_block = block_number,
drift,
"Re-pinned cache to current block"
);
}
}
}
#[instrument(level = "trace", skip(self))]
pub async fn ensure_account(&mut self, address: Address) -> Result<()> {
if self.db.cache.accounts.contains_key(&address) {
return Ok(());
}
use revm::database_interface::DatabaseRef;
let info = self
.backend
.basic_ref(address)
.map_err(|e| CacheError::AccountFetch {
address,
details: format!("{e:?}"),
})?;
if let Some(info) = info {
self.db.insert_account_info(address, info);
}
Ok(())
}
pub fn read_storage_slot(&mut self, address: Address, slot: U256) -> Result<U256> {
use revm::database_interface::DatabaseRef;
self.backend
.storage_ref(address, slot)
.map_err(|e| CacheError::StorageRead {
address,
slot,
details: e.to_string(),
})
}
pub fn insert_storage_slot(&mut self, address: Address, slot: U256, value: U256) -> Result<()> {
self.db
.insert_account_storage(address, slot, value)
.map_err(|e| CacheError::StorageInsert {
address,
slot,
details: e.to_string(),
})?;
Ok(())
}
pub fn seed_erc20_balance_slots(&mut self, slots: impl IntoIterator<Item = (Address, U256)>) {
for (token, slot) in slots {
self.erc20_balance_slots.insert(
token,
TrackedMapping::new(token, slot, SlotLayout::SolidityMapping),
);
}
}
pub fn seed_erc20_balance_layouts(
&mut self,
mappings: impl IntoIterator<Item = TrackedMapping>,
) {
for tracked in mappings {
self.erc20_balance_slots.insert(tracked.contract, tracked);
}
}
pub fn insert_mapping_storage_slot(
&mut self,
contract: Address,
slot: U256,
slot_address: Address,
value: U256,
) -> Result<()> {
let hashed_balance_slot = keccak256((slot_address, slot).abi_encode());
self.db
.insert_account_storage(contract, hashed_balance_slot.into(), value)
.map_err(|e| CacheError::StorageInsert {
address: contract,
slot: hashed_balance_slot.into(),
details: e.to_string(),
})?;
Ok(())
}
pub fn call_raw_with_inspector<I>(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
tx: &TxConfig,
inspector: I,
) -> Result<(ExecutionResult, I)>
where
I: for<'a> revm::Inspector<
Context<
BlockEnv,
TxEnv,
CfgEnv,
&'a mut ForkCacheDB,
Journal<&'a mut ForkCacheDB>,
(),
>,
>,
{
let tx_env = Self::build_tx_env_with(from, to, calldata, tx)?;
let mut evm = self.build_evm_with_inspector(inspector);
let checkpoint = evm.journaled_state.checkpoint();
let result = evm.inspect_one_tx(tx_env);
evm.journaled_state.checkpoint_revert(checkpoint);
let inspector = evm.inspector;
result.map(|r| (r, inspector)).map_err(CacheError::transact)
}
pub fn trace_hashed_slots(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
known_keys: &[B256],
) -> Result<Vec<HashSlotAccess>> {
let (_result, probe) = self.call_raw_with_inspector(
from,
to,
calldata,
&TxConfig::default(),
HashStorageProbe::new(),
)?;
Ok(probe.accesses(known_keys))
}
pub fn discover_erc20_balance_slot(
&mut self,
token: Address,
owner: Address,
) -> Result<Option<HashSlotAccess>> {
let calldata = Bytes::from(IERC20::balanceOfCall { target: owner }.abi_encode());
let (result, probe) = self.call_raw_with_inspector(
owner,
token,
calldata,
&TxConfig::default(),
HashStorageProbe::new(),
)?;
let ret = match result {
ExecutionResult::Success { output, .. } => output.into_data(),
_ => return Ok(None),
};
let ret_val = if ret.len() >= 32 {
U256::from_be_slice(&ret[..32])
} else {
U256::from_be_slice(&ret)
};
let owner_word = owner.into_word();
let best = probe
.accesses(&[owner_word])
.into_iter()
.filter(|a| a.keyed_by(owner_word))
.max_by_key(|a| (a.value == ret_val, a.confidence, std::cmp::Reverse(a.depth)));
Ok(best)
}
pub fn track_erc20_balances(
&mut self,
token: Address,
holders: impl IntoIterator<Item = Address>,
) -> Result<Option<TrackedBalances>> {
let holders: Vec<Address> = holders.into_iter().collect();
let tracked = if let Some(t) = self.erc20_balance_slots.get(&token).copied() {
t
} else {
let Some(&probe_holder) = holders.first() else {
return Ok(None);
};
let Some(tracked) = self
.discover_erc20_balance_slot(token, probe_holder)?
.and_then(|access| access.as_tracked(token))
else {
return Ok(None);
};
self.erc20_balance_slots.insert(token, tracked);
tracked
};
let pairs = tracked
.slots_for(holders.iter().map(|h| h.into_word()))
.into_iter()
.map(|(key, slot)| (Address::from_word(key), slot))
.collect();
Ok(Some((tracked, pairs)))
}
pub fn set_erc20_allowance(
&mut self,
token: Address,
owner: Address,
spender: Address,
amount: U256,
) -> Result<bool> {
let calldata = Bytes::from(IERC20::allowanceCall { owner, spender }.abi_encode());
let known = [owner.into_word(), spender.into_word()];
let (owner_word, spender_word) = (owner.into_word(), spender.into_word());
let target = self
.trace_hashed_slots(owner, token, calldata, &known)?
.into_iter()
.filter(|a| a.keyed_by(owner_word) && a.keyed_by(spender_word))
.max_by_key(|a| (a.depth, a.confidence));
let Some(target) = target else {
return Ok(false);
};
self.insert_storage_slot(token, U256::from_be_slice(target.slot.as_slice()), amount)?;
Ok(self.erc20_allowance(token, owner, spender)? == amount)
}
pub fn write_mapping_entry(
&mut self,
tracked: &TrackedMapping,
key: B256,
value: U256,
) -> Result<B256> {
let slot = tracked
.slot_for(key)
.ok_or_else(|| CacheError::StorageInsert {
address: tracked.contract,
slot: U256::ZERO,
details: format!(
"layout {} does not support single-key slot derivation",
tracked.layout
),
})?;
self.insert_storage_slot(
tracked.contract,
U256::from_be_slice(slot.as_slice()),
value,
)?;
Ok(slot)
}
pub fn mock_overlay(&mut self) -> EvmOverlay {
EvmOverlay::new(self.snapshot(), Some(self.backend.clone()))
}
pub fn set_erc20_balance_with_slot_scan(
&mut self,
token: Address,
owner: Address,
amount: U256,
max_slot: u16,
) -> Result<bool> {
let owner_word = owner.into_word();
if let Some(tracked) = self.erc20_balance_slots.get(&token).copied() {
self.write_mapping_entry(&tracked, owner_word, amount)?;
if self.erc20_balance_of(token, owner)? == amount {
return Ok(true);
}
self.erc20_balance_slots.remove(&token);
}
if let Some(tracked) = self
.discover_erc20_balance_slot(token, owner)?
.and_then(|access| access.as_tracked(token))
{
self.write_mapping_entry(&tracked, owner_word, amount)?;
if self.erc20_balance_of(token, owner)? == amount {
self.erc20_balance_slots.insert(token, tracked);
return Ok(true);
}
}
let Some(discovered_slot) =
self.discover_erc20_balance_slot_with_scan(token, owner, max_slot)?
else {
return Ok(false);
};
let tracked = TrackedMapping::new(token, discovered_slot, SlotLayout::SolidityMapping);
self.write_mapping_entry(&tracked, owner_word, amount)?;
let verified = self.erc20_balance_of(token, owner)? == amount;
if verified {
self.erc20_balance_slots.insert(token, tracked);
} else {
self.erc20_balance_slots.remove(&token);
}
Ok(verified)
}
fn discover_erc20_balance_slot_with_scan(
&mut self,
token: Address,
owner: Address,
max_slot: u16,
) -> Result<Option<U256>> {
if let Some(tracked) = self.erc20_balance_slots.get(&token) {
return Ok(Some(tracked.base_slot));
}
let baseline_snapshot = self.checkpoint();
let baseline_balance = self.erc20_balance_of(token, owner)?;
let mut probe = U256::from(0xDEAD_BEEF_u64);
if probe == baseline_balance {
probe = baseline_balance.saturating_add(U256::from(1u64));
}
if probe == baseline_balance {
probe = U256::MAX;
}
for slot_idx in 0..=max_slot {
self.restore(baseline_snapshot.clone());
let slot = U256::from(slot_idx);
self.insert_mapping_storage_slot(token, slot, owner, probe)?;
if self.erc20_balance_of(token, owner)? == probe {
self.restore(baseline_snapshot);
self.erc20_balance_slots.insert(
token,
TrackedMapping::new(token, slot, SlotLayout::SolidityMapping),
);
return Ok(Some(slot));
}
}
self.restore(baseline_snapshot);
Ok(None)
}
#[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
pub fn call(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
commit: bool,
) -> Result<ExecutionResult> {
self.call_raw(from, to, calldata, commit)
}
#[instrument(level = "debug", skip(self, calldata), fields(calldata_len = calldata.len()))]
pub fn call_raw(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
commit: bool,
) -> Result<ExecutionResult> {
self.call_raw_with(from, to, calldata, commit, &TxConfig::default())
}
pub fn call_sol<C>(&mut self, to: Address, call: C) -> Result<C::Return>
where
C: SolCall,
{
self.call_sol_from(Address::ZERO, to, call)
}
pub fn call_sol_from<C>(&mut self, from: Address, to: Address, call: C) -> Result<C::Return>
where
C: SolCall,
{
self.call_sol_with_commit(from, to, call, &TxConfig::default(), false)
}
pub fn call_sol_with<C>(
&mut self,
from: Address,
to: Address,
call: C,
tx: &TxConfig,
) -> Result<C::Return>
where
C: SolCall,
{
self.call_sol_with_commit(from, to, call, tx, false)
}
pub fn transact_sol<C>(
&mut self,
from: Address,
to: Address,
call: C,
tx: &TxConfig,
) -> Result<C::Return>
where
C: SolCall,
{
self.call_sol_with_commit(from, to, call, tx, true)
}
#[instrument(level = "debug", skip(self, calldata, tx), fields(calldata_len = calldata.len()))]
pub fn call_raw_with(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
commit: bool,
tx: &TxConfig,
) -> Result<ExecutionResult> {
let tx_env = Self::build_tx_env_with(from, to, calldata, tx)?;
let mut evm = self.build_evm();
if commit {
return evm.transact_commit(tx_env).map_err(CacheError::transact);
}
let checkpoint = evm.journaled_state.checkpoint();
let result = evm.transact_one(tx_env);
evm.journaled_state.checkpoint_revert(checkpoint);
result.map_err(CacheError::transact)
}
pub fn call_raw_with_access_list(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
) -> Result<(ExecutionResult, StorageAccessList)> {
let tx = Self::build_tx_env(from, to, calldata)?;
let mut evm = self.build_evm();
let checkpoint = evm.journaled_state.checkpoint();
match evm.transact_one(tx) {
Ok(result) => {
let mut access_list = StorageAccessList::default();
for (address, account) in evm.journaled_state.state.iter() {
if account.is_touched() {
access_list.accounts.insert(*address);
for (slot_key, _) in account.storage.iter() {
access_list.slots.insert((*address, *slot_key));
}
}
}
evm.journaled_state.checkpoint_revert(checkpoint);
Ok((result, access_list))
}
Err(e) => {
evm.journaled_state.checkpoint_revert(checkpoint);
Err(CacheError::transact(e))
}
}
}
pub fn call_logs(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
commit: bool,
) -> Result<(Vec<Log>, u64)> {
let result = self.call(from, to, calldata, commit)?;
if let ExecutionResult::Success { logs, gas_used, .. } = result {
Ok((logs, gas_used))
} else {
Err(CacheError::CallNotSuccessful {
result: format!("{result:?}"),
})
}
}
pub fn erc20_balance_of(&mut self, token: Address, owner: Address) -> Result<U256> {
let call = IERC20::balanceOfCall { target: owner };
let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
match result {
ExecutionResult::Success { output, .. } => {
let out = output.into_data();
let balance = IERC20::balanceOfCall::abi_decode_returns(&out).map_err(|e| {
CacheError::Decode {
what: "ERC20 balanceOf return data",
details: format!("{e:?}"),
}
})?;
Ok(balance)
}
_ => Err(CacheError::CallNotSuccessful {
result: format!("{result:?}"),
}),
}
}
pub fn erc20_allowance(
&mut self,
token: Address,
owner: Address,
spender: Address,
) -> Result<U256> {
let call = IERC20::allowanceCall { owner, spender };
let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
match result {
ExecutionResult::Success { output, .. } => {
let out = output.into_data();
let allowance = IERC20::allowanceCall::abi_decode_returns(&out).map_err(|e| {
CacheError::Decode {
what: "ERC20 allowance return data",
details: format!("{e:?}"),
}
})?;
Ok(allowance)
}
_ => Err(CacheError::CallNotSuccessful {
result: format!("{result:?}"),
}),
}
}
pub fn erc20_decimals(&mut self, token: Address) -> Result<u8> {
if let Some(decimals) = self.token_decimals.get(&token) {
return Ok(*decimals);
}
let call = IERC20::decimalsCall {};
let result = self.call_raw(Address::ZERO, token, Bytes::from(call.abi_encode()), false)?;
match result {
ExecutionResult::Success { output, .. } => {
let out = output.into_data();
let decimals = IERC20::decimalsCall::abi_decode_returns(&out).map_err(|e| {
CacheError::Decode {
what: "ERC20 decimals return data",
details: format!("{e:?}"),
}
})?;
self.token_decimals.insert(token, decimals);
self.immutable_cache.set_token_decimals(token, decimals);
Ok(decimals)
}
_ => Err(CacheError::CallNotSuccessful {
result: format!("{result:?}"),
}),
}
}
pub fn immutable_cache(&self) -> &ImmutableDataCache {
&self.immutable_cache
}
pub fn immutable_cache_mut(&mut self) -> &mut ImmutableDataCache {
&mut self.immutable_cache
}
pub fn has_contract_storage(&self, address: Address) -> bool {
let storage = self.blockchain_db.storage().read();
storage
.get(&address)
.map(|slots| !slots.is_empty())
.unwrap_or(false)
}
pub fn contract_storage_slot_count(&self, address: Address) -> usize {
let storage = self.blockchain_db.storage().read();
storage.get(&address).map(|slots| slots.len()).unwrap_or(0)
}
pub fn shared_memory_stats(&self) -> (usize, usize) {
let buffer = self.shared_memory_buffer.borrow();
(buffer.capacity(), buffer.len())
}
pub fn log_shared_memory_stats(&self) {
let (capacity, len) = self.shared_memory_stats();
debug!(
capacity_bytes = capacity,
capacity_kb = capacity / 1024,
current_len = len,
"Shared memory buffer stats (peak capacity across simulations)"
);
}
pub fn reserve_shared_memory(&mut self, capacity: usize) {
let mut buffer = self.shared_memory_buffer.borrow_mut();
let current_capacity = buffer.capacity();
if current_capacity < capacity {
buffer.reserve(capacity - current_capacity);
debug!(
new_capacity = buffer.capacity(),
requested = capacity,
"Reserved shared memory buffer capacity"
);
}
drop(buffer);
self.shared_memory_capacity = self.shared_memory_capacity.max(capacity);
}
pub fn shared_memory_capacity(&self) -> usize {
self.shared_memory_capacity
}
pub fn storage_batch_config(&self) -> StorageBatchConfig {
self.storage_batch_config
}
pub fn purge_contract_storage(&mut self, address: Address) -> usize {
self.apply_update(&StateUpdate::purge(address, PurgeScope::AllStorage))
.purged
.first()
.map(|rec| rec.slots_removed)
.unwrap_or(0)
}
fn purge_contract_storage_inner(&mut self, address: Address) -> usize {
let cache_db_cleared = if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
let count = db_account.storage.len();
db_account.storage.clear();
count
} else {
0
};
let backend_cleared = {
let mut storage = self.blockchain_db.storage().write();
if let Some(slots) = storage.remove(&address) {
slots.len()
} else {
0
}
};
if cache_db_cleared > 0 || backend_cleared > 0 {
debug!(
contract = %address,
cache_db_slots = cache_db_cleared,
backend_slots = backend_cleared,
"purged contract storage from both cache layers"
);
}
self.mark_base_dirty(address);
backend_cleared
}
pub fn purge_contract_slots(&mut self, address: Address, slots: &[U256]) -> usize {
self.apply_update(&StateUpdate::purge(
address,
PurgeScope::Slots(slots.to_vec()),
))
.purged
.first()
.map(|rec| rec.slots_removed)
.unwrap_or(0)
}
fn purge_contract_slots_inner(&mut self, address: Address, slots: &[U256]) -> usize {
let mut cache_db_removed = 0usize;
let mut backend_removed = 0usize;
if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
for slot in slots {
if db_account.storage.remove(slot).is_some() {
cache_db_removed += 1;
}
}
}
{
let mut storage = self.blockchain_db.storage().write();
if let Some(address_storage) = storage.get_mut(&address) {
for slot in slots {
if address_storage.remove(slot).is_some() {
backend_removed += 1;
}
}
}
}
if cache_db_removed > 0 || backend_removed > 0 {
trace!(
contract = %address,
requested = slots.len(),
cache_db_removed,
backend_removed,
"selectively purged contract storage slots from both cache layers"
);
}
self.mark_base_dirty(address);
backend_removed
}
pub fn purge_contracts_storage(
&mut self,
addresses: impl IntoIterator<Item = Address>,
) -> usize {
let mut total_purged = 0usize;
for address in addresses {
if let Some(db_account) = self.db.cache.accounts.get_mut(&address) {
db_account.storage.clear();
}
let mut storage = self.blockchain_db.storage().write();
if let Some(slots) = storage.remove(&address) {
let count = slots.len();
if count > 0 {
debug!(
contract = %address,
slots_removed = count,
"purged contract storage from both cache layers"
);
}
total_purged += count;
}
}
if total_purged > 0 {
debug!(
total_slots_purged = total_purged,
"purged contract storage from both cache layers"
);
}
self.invalidate_base();
total_purged
}
pub fn purge_all_storage(&mut self) -> usize {
let mut cache_db_cleared = 0usize;
for db_account in self.db.cache.accounts.values_mut() {
cache_db_cleared += db_account.storage.len();
db_account.storage.clear();
}
let (total_slots, contract_count) = {
let mut storage = self.blockchain_db.storage().write();
let total_slots: usize = storage.values().map(|s| s.len()).sum();
let contract_count = storage.len();
storage.clear();
(total_slots, contract_count)
};
if total_slots > 0 || cache_db_cleared > 0 {
warn!(
contracts_cleared = contract_count,
backend_slots_purged = total_slots,
cache_db_slots_purged = cache_db_cleared,
"purged ALL storage from both cache layers (full refresh)"
);
}
self.invalidate_base();
total_slots
}
pub fn enumerate_contract_slots(&self, address: Address) -> Vec<U256> {
let mut slots: HashSet<U256> = HashSet::new();
if let Some(db_account) = self.db.cache.accounts.get(&address) {
slots.extend(db_account.storage.keys().copied());
}
let storage = self.blockchain_db.storage().read();
if let Some(backend_slots) = storage.get(&address) {
slots.extend(backend_slots.keys().copied());
}
slots.into_iter().collect()
}
pub fn all_cached_contract_addresses(&self) -> Vec<Address> {
let mut addrs: HashSet<Address> = HashSet::new();
for (addr, account) in &self.db.cache.accounts {
if !account.storage.is_empty() {
addrs.insert(*addr);
}
}
let storage = self.blockchain_db.storage().read();
for addr in storage.keys() {
addrs.insert(*addr);
}
addrs.into_iter().collect()
}
pub fn cache_db_storage_slot_count(&self, address: Address) -> usize {
self.db
.cache
.accounts
.get(&address)
.map(|a| a.storage.len())
.unwrap_or(0)
}
pub fn simulate_call_with_balance_deltas(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
owner: Address,
tokens: impl IntoIterator<Item = Address>,
commit: bool,
) -> Result<CallSimulationResult> {
let token_list: Vec<Address> = tokens.into_iter().collect();
let mut pre_balances = HashMap::with_capacity(token_list.len());
let mut access_lists = Vec::with_capacity(token_list.len().saturating_mul(2) + 1);
for token in &token_list {
let mut evm = self.build_evm();
let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
let (balance, access_list) =
Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner)?;
Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
pre_balances.insert(*token, balance);
access_lists.push(access_list);
}
let tx = Self::build_tx_env(from, to, calldata)?;
let mut evm = self.build_evm();
let synthetic_beneficiary = Self::seed_synthetic_beneficiary(&mut evm);
let target_checkpoint = evm.journaled_state.checkpoint();
let result = evm.transact_one(tx).map_err(CacheError::transact)?;
let (logs, gas_used, output) = match result {
ExecutionResult::Success {
logs,
gas_used,
output,
..
} => (logs, gas_used, output.into_data()),
_ => {
evm.journaled_state.checkpoint_revert(target_checkpoint);
Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
return Err(CacheError::CallNotSuccessful {
result: format!("{result:?}"),
});
}
};
access_lists.push(extract_access_list(&evm.journaled_state.state));
let mut token_deltas = HashMap::with_capacity(token_list.len());
for token in &token_list {
let (post, access_list) =
match Self::erc20_balance_of_in_evm_isolated(&mut evm, from, *token, owner) {
Ok(result) => result,
Err(err) => {
evm.journaled_state.checkpoint_revert(target_checkpoint);
Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
return Err(err);
}
};
let pre = pre_balances.get(token).copied().unwrap_or_default();
token_deltas.insert(*token, I256::from_raw(post) - I256::from_raw(pre));
access_lists.push(access_list);
}
let access_list = merge_access_lists(access_lists);
if commit {
Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
evm.commit_inner();
} else {
evm.journaled_state.checkpoint_revert(target_checkpoint);
Self::remove_synthetic_beneficiary(&mut evm, synthetic_beneficiary);
}
Ok(CallSimulationResult {
status: SimStatus::Success,
gas_used,
token_deltas,
logs,
access_list,
output,
})
}
#[instrument(level = "debug", skip(self, calldata, tokens), fields(calldata_len = calldata.len()))]
pub fn simulate_with_transfer_tracking(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
owner: Address,
tokens: Option<impl IntoIterator<Item = Address>>,
commit: bool,
) -> SimulationResult<CallSimulationResult> {
let tx = Self::build_tx_env(from, to, calldata).map_err(SimError::from)?;
let inspector = TransferInspector::new();
let mut evm = self.build_evm_with_inspector(inspector);
let checkpoint = evm.journaled_state.checkpoint();
let result = evm
.inspect_one_tx(tx)
.map_err(|e| SimError::Other(SimHostError::transact(e)));
match result {
Ok(ExecutionResult::Success {
logs,
gas_used,
output,
..
}) => {
let token_deltas = if let Some(token_list) = tokens {
evm.inspector.balance_deltas_for_tokens(owner, token_list)
} else {
evm.inspector.balance_deltas(owner)
};
let memory_capacity = evm.ctx.local.shared_memory_buffer.borrow().capacity();
trace!(
memory_capacity_bytes = memory_capacity,
memory_capacity_kb = memory_capacity / 1024,
"EVM shared memory buffer capacity after simulation"
);
let access_list = extract_access_list(&evm.journaled_state.state);
if commit {
evm.commit_inner();
} else {
evm.journaled_state.checkpoint_revert(checkpoint);
}
Ok(CallSimulationResult {
status: SimStatus::Success,
gas_used,
token_deltas,
logs,
access_list,
output: output.into_data(),
})
}
Ok(ExecutionResult::Revert { gas_used, output }) => {
evm.journaled_state.checkpoint_revert(checkpoint);
Err(SimulationError::from_revert(gas_used, output).into())
}
Ok(ExecutionResult::Halt { reason, gas_used }) => {
evm.journaled_state.checkpoint_revert(checkpoint);
Err(SimError::Halt {
reason: format!("{reason:?}"),
gas_used,
})
}
Err(err) => {
evm.journaled_state.checkpoint_revert(checkpoint);
Err(err)
}
}
}
pub fn simulate_with_transfer_tracking_raw(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
owner: Address,
tokens: Option<impl IntoIterator<Item = Address>>,
commit: bool,
) -> SimulationResult<CallSimulationResult> {
self.simulate_with_transfer_tracking(from, to, calldata, owner, tokens, commit)
}
pub fn simulate_bundle(
&mut self,
txs: &[crate::bundle::BundleTx],
opts: &crate::bundle::BundleOptions,
) -> SimulationResult<crate::bundle::BundleResult> {
let snapshot = self.snapshot();
let mut overlay = EvmOverlay::new(snapshot, None);
overlay.simulate_bundle(txs, opts)
}
pub fn deploy_contract(&mut self, from: Address, creation_code: Bytes) -> Result<Address> {
let tx = TxEnv::builder()
.caller(from)
.kind(TxKind::Create)
.data(creation_code)
.value(U256::ZERO)
.build()
.map_err(CacheError::tx_env)?;
let mut evm = self.build_evm();
evm.cfg.limit_contract_code_size = Some(usize::MAX);
let result = evm.transact_commit(tx).map_err(CacheError::transact)?;
match result {
ExecutionResult::Success { output, .. } => {
let address = output
.address()
.copied()
.ok_or(CacheError::DeploymentMissingAddress)?;
let code_hash = self
.db
.cache
.accounts
.get(&address)
.map(|account| account.info.code_hash)
.unwrap_or(revm::primitives::KECCAK_EMPTY);
self.code_seeds
.insert(address, CodeSeedState::Etched { code_hash });
Ok(address)
}
ExecutionResult::Revert { output, .. } => Err(CacheError::DeploymentReverted {
output_hex: alloy_primitives::hex::encode(&output),
}),
ExecutionResult::Halt { reason, .. } => Err(CacheError::DeploymentHalted {
reason: format!("{reason:?}"),
}),
}
}
pub fn override_account_code(&mut self, source: Address, target: Address) -> Result<()> {
self.override_account_code_with_missing_target(source, target, MissingTargetBehavior::Error)
}
pub fn override_or_create_account_code(
&mut self,
source: Address,
target: Address,
) -> Result<()> {
self.override_account_code_with_missing_target(
source,
target,
MissingTargetBehavior::Create,
)
}
pub fn override_account_code_with_missing_target(
&mut self,
source: Address,
target: Address,
missing_target: MissingTargetBehavior,
) -> Result<()> {
let source_code = self
.db
.cache
.accounts
.get(&source)
.and_then(|a| a.info.code.clone())
.ok_or(CacheError::MissingSourceBytecode {
source_address: source,
})?;
Self::ensure_runtime_code(source, Some(&source_code), "source")?;
let code_hash = source_code.hash_slow();
debug!(
source = %source,
target = %target,
code_size = source_code.len(),
"Overriding account bytecode"
);
let mut target_info = self.target_account_info(target, missing_target)?;
if matches!(missing_target, MissingTargetBehavior::Error) {
Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")?;
}
target_info.code = Some(source_code);
target_info.code_hash = code_hash;
self.db.insert_account_info(target, target_info.clone());
{
let mut accounts = self.blockchain_db.accounts().write();
accounts.insert(target, target_info);
}
self.mark_base_dirty(target);
self.code_seeds
.insert(target, CodeSeedState::Etched { code_hash });
Ok(())
}
pub fn verify_code_seeds(&mut self) -> Result<CodeVerifyReport> {
let pending = self.pending_code_seeds();
if pending.is_empty() {
return Ok(CodeVerifyReport::default());
}
let fetcher = self
.account_fields_fetcher
.clone()
.ok_or(CacheError::MissingAccountFieldsFetcher)?;
let mut report = CodeVerifyReport::default();
let (host, query): (Vec<Address>, Vec<Address>) = pending
.into_iter()
.partition(|address| *address == crate::multicall::MULTICALL3_ADDRESS);
for address in host {
report.unverifiable.push((
address,
"the account-fields extractor is hosted at this address under the eth_call \
override; verify it via the eth_getProof path instead"
.to_string(),
));
}
if query.is_empty() {
return Ok(report);
}
let samples = match (fetcher)(query.clone(), self.block) {
Ok(samples) => samples,
Err(error) => {
let reason = error.to_string();
report
.unverifiable
.extend(query.into_iter().map(|address| (address, reason.clone())));
return Ok(report);
}
};
let by_address: HashMap<Address, AccountFieldsSample> = samples.into_iter().collect();
let verified_at_block = self.block_number.unwrap_or_default();
for address in query {
let Some(CodeSeedState::Pending {
code_hash: expected,
}) = self.code_seeds.get(&address).cloned()
else {
continue;
};
let Some(sample) = by_address.get(&address) else {
report.unverifiable.push((
address,
"account-fields fetcher returned no sample for this address".to_string(),
));
continue;
};
if sample.code_hash == expected {
self.code_seeds.insert(
address,
CodeSeedState::Verified {
code_hash: expected,
verified_at_block,
},
);
self.materialize_verified_balance(address, sample.balance);
report.verified.push(address);
} else if sample.code_hash == B256::ZERO {
self.purge_account(address);
report.not_deployed.push(address);
} else if sample.code_hash == revm::primitives::KECCAK_EMPTY {
self.purge_account(address);
report.codeless.push(address);
} else {
self.purge_account(address);
report.mismatched.push(CodeMismatch {
address,
expected,
actual: sample.code_hash,
});
}
}
Ok(report)
}
fn materialize_verified_balance(&mut self, address: Address, balance: U256) {
if let Some(account) = self.db.cache.accounts.get_mut(&address) {
account.info.balance = balance;
}
{
let mut accounts = self.blockchain_db.accounts().write();
if let Some(info) = accounts.get_mut(&address) {
info.balance = balance;
}
}
self.mark_base_dirty(address);
}
fn local_account_info(&self, address: Address) -> Option<AccountInfo> {
if let Some(account) = self.db.cache.accounts.get(&address) {
return Some(account.info.clone());
}
self.blockchain_db.accounts().read().get(&address).cloned()
}
fn write_marked_code(&mut self, address: Address, info: AccountInfo) {
self.db.insert_account_info(address, info.clone());
{
let mut accounts = self.blockchain_db.accounts().write();
accounts.insert(address, info);
}
self.mark_base_dirty(address);
self.bump_snapshot_generation();
}
pub fn seed_account_code(&mut self, address: Address, code: Bytes) -> Result<B256> {
self.seed_account_code_with(address, code, 1, U256::ZERO)
}
pub fn seed_account_code_with(
&mut self,
address: Address,
code: Bytes,
nonce: u64,
balance: U256,
) -> Result<B256> {
if code.is_empty() {
return Err(CacheError::CodeSeedEmpty { address });
}
let bytecode = Bytecode::new_raw(code);
let code_hash = bytecode.hash_slow();
if !self.code_seeds.contains_key(&address)
&& let Some(existing) = self.local_account_info(address)
{
if existing.code_hash == code_hash {
if existing
.code
.as_ref()
.is_none_or(|existing_code| existing_code.is_empty())
{
let mut info = existing;
info.code = Some(bytecode);
info.code_hash = code_hash;
self.write_marked_code(address, info);
}
self.code_seeds.insert(
address,
CodeSeedState::Verified {
code_hash,
verified_at_block: self.block_number.unwrap_or_default(),
},
);
return Ok(code_hash);
}
return Err(CacheError::CodeSeedConflict {
address,
cached: existing.code_hash,
seeded: code_hash,
});
}
let mut info = self.local_account_info(address).unwrap_or(AccountInfo {
balance,
nonce,
code_hash: revm::primitives::KECCAK_EMPTY,
code: None,
account_id: None,
});
info.code = Some(bytecode);
info.code_hash = code_hash;
self.write_marked_code(address, info);
self.code_seeds
.insert(address, CodeSeedState::Pending { code_hash });
Ok(code_hash)
}
pub fn etch_account_code(&mut self, address: Address, code: Bytes) -> Result<B256> {
if code.is_empty() {
return Err(CacheError::CodeSeedEmpty { address });
}
let bytecode = Bytecode::new_raw(code);
let code_hash = bytecode.hash_slow();
let mut info = self.local_account_info(address).unwrap_or_default();
info.code = Some(bytecode);
info.code_hash = code_hash;
self.write_marked_code(address, info);
self.code_seeds
.insert(address, CodeSeedState::Etched { code_hash });
Ok(code_hash)
}
pub fn code_seed_state(&self, address: &Address) -> Option<&CodeSeedState> {
self.code_seeds.get(address)
}
pub fn pending_code_seeds(&self) -> Vec<Address> {
let mut pending: Vec<Address> = self
.code_seeds
.iter()
.filter_map(|(addr, state)| {
matches!(state, CodeSeedState::Pending { .. }).then_some(*addr)
})
.collect();
pending.sort();
pending
}
pub fn etched_accounts(&self) -> Vec<Address> {
let mut etched: Vec<Address> = self
.code_seeds
.iter()
.filter_map(|(addr, state)| {
matches!(state, CodeSeedState::Etched { .. }).then_some(*addr)
})
.collect();
etched.sort();
etched
}
pub(crate) fn require_contract_target(&self, target: Address) -> Result<()> {
let target_info = self.target_account_info(target, MissingTargetBehavior::Error)?;
Self::ensure_runtime_code(target, target_info.code.as_ref(), "target")
}
fn target_account_info(
&self,
target: Address,
missing_target: MissingTargetBehavior,
) -> Result<AccountInfo> {
if let Some(account) = self.db.cache.accounts.get(&target) {
if !matches!(account.account_state, AccountState::NotExisting) {
return Ok(account.info.clone());
}
}
match missing_target {
MissingTargetBehavior::Create => Ok(AccountInfo::default()),
MissingTargetBehavior::Error => {
use revm::database_interface::DatabaseRef;
self.backend
.basic_ref(target)
.map_err(|e| CacheError::TargetAccountFetch {
target,
details: format!("{e:?}"),
})?
.ok_or(CacheError::MissingTargetAccount { target })
}
}
}
fn ensure_runtime_code(address: Address, code: Option<&Bytecode>, role: &str) -> Result<()> {
if code.is_some_and(|code| !code.is_empty()) {
return Ok(());
}
Err(CacheError::MissingRuntimeCode {
role: match role {
"source" => "source",
"target" => "target",
_ => "account",
},
address,
})
}
}
impl crate::events::StateView for EvmCache {
fn storage(&self, address: Address, slot: U256) -> Option<U256> {
self.cached_storage_value(address, slot)
}
}
impl EvmCache {
fn make_local_context(&self) -> LocalContext {
self.shared_memory_buffer.borrow_mut().clear();
LocalContext {
shared_memory_buffer: self.shared_memory_buffer.clone(),
precompile_error_message: None,
}
}
fn build_evm(&mut self) -> CacheEvm<'_> {
let local = self.make_local_context();
let chain_id = self.chain_id;
let mut evm = Context::mainnet()
.with_db(&mut self.db)
.with_local(local)
.modify_cfg_chained(|cfg| {
cfg.disable_nonce_check = true;
cfg.disable_eip3607 = true;
cfg.disable_base_fee = true;
cfg.disable_balance_check = true;
cfg.chain_id = chain_id;
cfg.limit_contract_code_size = None;
cfg.tx_chain_id_check = false;
cfg.spec = self.spec_id;
})
.build_mainnet();
let timestamp = self
.timestamp_override
.unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
evm.block.timestamp = U256::from(timestamp);
if let Some(number) = self.block_number {
evm.block.number = U256::from(number);
}
if let Some(basefee) = self.basefee {
evm.block.basefee = basefee;
}
if let Some(coinbase) = self.coinbase {
evm.block.beneficiary = coinbase;
}
if let Some(prevrandao) = self.prevrandao {
evm.block.prevrandao = Some(prevrandao);
}
if let Some(gas_limit) = self.block_gas_limit {
evm.block.gas_limit = gas_limit;
}
evm
}
fn build_evm_with_inspector<INSP>(&mut self, inspector: INSP) -> InspectorCacheEvm<'_, INSP> {
let local = self.make_local_context();
let chain_id = self.chain_id;
let mut evm = Context::mainnet()
.with_db(&mut self.db)
.with_local(local)
.modify_cfg_chained(|cfg| {
cfg.disable_nonce_check = true;
cfg.disable_eip3607 = true;
cfg.disable_base_fee = true;
cfg.disable_balance_check = true;
cfg.chain_id = chain_id;
cfg.limit_contract_code_size = None;
cfg.tx_chain_id_check = false;
cfg.spec = self.spec_id;
})
.build_mainnet_with_inspector(inspector);
let timestamp = self
.timestamp_override
.unwrap_or_else(|| unix_timestamp_secs_saturating(SystemTime::now()));
evm.block.timestamp = U256::from(timestamp);
if let Some(number) = self.block_number {
evm.block.number = U256::from(number);
}
if let Some(basefee) = self.basefee {
evm.block.basefee = basefee;
}
if let Some(coinbase) = self.coinbase {
evm.block.beneficiary = coinbase;
}
if let Some(prevrandao) = self.prevrandao {
evm.block.prevrandao = Some(prevrandao);
}
if let Some(gas_limit) = self.block_gas_limit {
evm.block.gas_limit = gas_limit;
}
evm
}
fn build_tx_env(from: Address, to: Address, calldata: Bytes) -> Result<TxEnv> {
Self::build_tx_env_with(from, to, calldata, &TxConfig::default())
}
fn build_tx_env_with(
from: Address,
to: Address,
calldata: Bytes,
tx: &TxConfig,
) -> Result<TxEnv> {
let mut builder = TxEnv::builder()
.caller(from)
.kind(TxKind::Call(to))
.data(calldata)
.value(tx.value);
if let Some(gas_limit) = tx.gas_limit {
builder = builder.gas_limit(gas_limit);
}
if let Some(gas_price) = tx.gas_price {
builder = builder.gas_price(gas_price);
}
if let Some(nonce) = tx.nonce {
builder = builder.nonce(nonce);
}
if let Some(access_list) = &tx.access_list {
builder = builder.access_list(access_list.clone());
}
builder.build().map_err(CacheError::tx_env)
}
fn call_sol_with_commit<C>(
&mut self,
from: Address,
to: Address,
call: C,
tx: &TxConfig,
commit: bool,
) -> Result<C::Return>
where
C: SolCall,
{
let calldata = Bytes::from(call.abi_encode());
let result = self.call_raw_with(from, to, calldata, commit, tx)?;
Self::decode_sol_call_result::<C>(from, to, result)
}
fn decode_sol_call_result<C>(
from: Address,
to: Address,
result: ExecutionResult,
) -> Result<C::Return>
where
C: SolCall,
{
match result {
ExecutionResult::Success { output, .. } => {
let output = output.into_data();
C::abi_decode_returns(&output).map_err(|error| CacheError::SolCallDecode {
signature: C::SIGNATURE,
from,
to,
output_len: output.len(),
details: format!("{error:?}"),
})
}
other => Err(CacheError::SolCallFailed {
signature: C::SIGNATURE,
from,
to,
result: format!("{other:?}"),
}),
}
}
fn erc20_balance_of_in_evm(
evm: &mut CacheEvm<'_>,
caller: Address,
token: Address,
owner: Address,
) -> Result<U256> {
let call = IERC20::balanceOfCall { target: owner };
let tx = Self::build_tx_env(caller, token, Bytes::from(call.abi_encode()))?;
let result = evm.transact_one(tx).map_err(CacheError::transact)?;
match result {
ExecutionResult::Success { output, .. } => {
let out = output.into_data();
let balance = IERC20::balanceOfCall::abi_decode_returns(&out).map_err(|e| {
CacheError::Decode {
what: "ERC20 balanceOf return data",
details: format!("{e:?}"),
}
})?;
Ok(balance)
}
_ => Err(CacheError::CallNotSuccessful {
result: format!("{result:?}"),
}),
}
}
fn erc20_balance_of_in_evm_isolated(
evm: &mut CacheEvm<'_>,
caller: Address,
token: Address,
owner: Address,
) -> Result<(U256, AccessList)> {
let state_before = evm.journaled_state.state.clone();
let checkpoint = evm.journaled_state.checkpoint();
let result = Self::erc20_balance_of_in_evm(evm, caller, token, owner);
let access_list = extract_access_list(&evm.journaled_state.state);
evm.journaled_state.checkpoint_revert(checkpoint);
evm.journaled_state.state = state_before;
result.map(|balance| (balance, access_list))
}
fn seed_synthetic_beneficiary(evm: &mut CacheEvm<'_>) -> Option<Address> {
let beneficiary = evm.block.beneficiary;
if evm.journaled_state.state.contains_key(&beneficiary) {
return None;
}
evm.journaled_state
.state
.insert(beneficiary, Account::from(AccountInfo::default()));
Some(beneficiary)
}
fn remove_synthetic_beneficiary(evm: &mut CacheEvm<'_>, beneficiary: Option<Address>) {
if let Some(beneficiary) = beneficiary {
evm.journaled_state.state.remove(&beneficiary);
}
}
}
pub struct EvmSession<'a> {
evm: CacheEvm<'a>,
}
impl<'a> EvmSession<'a> {
pub fn call_raw(
&mut self,
from: Address,
to: Address,
calldata: Bytes,
commit: bool,
) -> Result<ExecutionResult> {
let tx = EvmCache::build_tx_env(from, to, calldata)?;
if commit {
self.evm.transact_one(tx).map_err(CacheError::transact)
} else {
let checkpoint = self.evm.journaled_state.checkpoint();
let result = self.evm.transact_one(tx);
self.evm.journaled_state.checkpoint_revert(checkpoint);
result.map_err(CacheError::transact)
}
}
pub fn commit(mut self) {
self.evm.commit_inner();
}
pub fn evm(&mut self) -> &mut CacheEvm<'a> {
&mut self.evm
}
}
impl Drop for EvmCache {
fn drop(&mut self) {
if self.cache_config.is_some() {
debug!("Flushing EVM cache on drop");
if let Err(e) = self.flush() {
warn!(error = %e, "Failed to flush EVM cache on drop");
}
}
}
}
#[cfg(test)]
mod shared_memory_capacity_tests {
use super::SharedMemoryCapacity as Cap;
#[test]
fn default_is_fixed_64k() {
assert_eq!(Cap::default(), Cap::Fixed(64 * 1024));
}
#[test]
fn fixed_ignores_loaded_slots() {
assert_eq!(Cap::Fixed(8_192).resolve(10_000_000), 8_192);
assert_eq!(Cap::Fixed(0).resolve(123), 0);
}
#[test]
fn auto_floors_clamps_and_scales() {
assert_eq!(Cap::Auto.resolve(0), Cap::MIN_AUTO);
assert_eq!(Cap::Auto.resolve(1_000), Cap::MIN_AUTO); assert_eq!(Cap::Auto.resolve(10_000), 160_000);
assert_eq!(Cap::Auto.resolve(100_000), 1_600_000);
assert_eq!(Cap::Auto.resolve(usize::MAX), Cap::MAX_AUTO);
assert_eq!(Cap::Auto.resolve(262_144), Cap::MAX_AUTO); }
}
#[cfg(test)]
mod core_tests {
use super::*;
#[test]
fn parses_prestate_diff_trace_values_and_cleared_slots() {
let trace = serde_json::json!([
{
"result": {
"pre": {
"0x4242424242424242424242424242424242424242": {
"storage": {
"0x01": "0x05",
"0x02": "0x06"
}
}
},
"post": {
"0x4242424242424242424242424242424242424242": {
"balance": 10,
"nonce": "0x0a",
"code": "0x6001",
"storage": {
"0x01": "0x0b"
}
}
}
}
}
]);
let diff = parse_block_state_diff_trace(&trace).unwrap();
assert_eq!(diff.accounts.len(), 1);
let account = &diff.accounts[0];
assert_eq!(account.address, Address::repeat_byte(0x42));
assert_eq!(account.balance, Some(U256::from(10)));
assert_eq!(account.nonce, Some(10));
assert_eq!(account.code, Some(Bytes::from(vec![0x60, 0x01])));
assert_eq!(
account.storage,
vec![
BlockStateStorageDiff {
slot: U256::from(1),
value: U256::from(11),
},
BlockStateStorageDiff {
slot: U256::from(2),
value: U256::ZERO,
},
]
);
}
#[test]
fn parses_prestate_diff_trace_account_deletion() {
let trace = serde_json::json!([
{
"result": {
"pre": {
"0x4242424242424242424242424242424242424242": {
"balance": "0x64",
"nonce": "0x01",
"code": "0x6001",
"storage": { "0x01": "0x05" }
},
"0x1111111111111111111111111111111111111111": {
"balance": "0x0a"
}
},
"post": {}
}
}
]);
let diff = parse_block_state_diff_trace(&trace).unwrap();
assert_eq!(diff.accounts.len(), 2);
let bare = &diff.accounts[0]; assert_eq!(bare.address, Address::repeat_byte(0x11));
assert_eq!(bare.balance, Some(U256::ZERO));
assert_eq!(bare.nonce, Some(0));
assert_eq!(bare.code, Some(Bytes::new()));
assert!(bare.storage.is_empty());
let stored = &diff.accounts[1];
assert_eq!(stored.address, Address::repeat_byte(0x42));
assert_eq!(stored.balance, Some(U256::ZERO));
assert_eq!(stored.nonce, Some(0));
assert_eq!(stored.code, Some(Bytes::new()));
assert_eq!(
stored.storage,
vec![BlockStateStorageDiff {
slot: U256::from(1),
value: U256::ZERO,
}]
);
}
#[test]
fn parses_prestate_diff_trace_deletion_then_recreation_keeps_final_state() {
let trace = serde_json::json!([
{
"result": {
"pre": {
"0x4242424242424242424242424242424242424242": { "balance": "0x64" }
},
"post": {}
}
},
{
"result": {
"pre": {},
"post": {
"0x4242424242424242424242424242424242424242": {
"balance": "0x07",
"nonce": "0x01",
"code": "0x6002"
}
}
}
}
]);
let diff = parse_block_state_diff_trace(&trace).unwrap();
assert_eq!(diff.accounts.len(), 1);
let account = &diff.accounts[0];
assert_eq!(account.balance, Some(U256::from(7)));
assert_eq!(account.nonce, Some(1));
assert_eq!(account.code, Some(Bytes::from(vec![0x60, 0x02])));
}
#[test]
fn snapshot_generation_bumps_on_writes_and_repins_not_prefetch() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
let addr = Address::repeat_byte(0x77);
let g0 = cache.snapshot_generation();
cache.apply_updates(&[StateUpdate::slot(addr, U256::from(1), U256::from(10))]);
let g1 = cache.snapshot_generation();
assert!(g1 > g0, "apply_updates must bump the generation");
cache.apply_update(&StateUpdate::slot(addr, U256::from(2), U256::from(20)));
let g2 = cache.snapshot_generation();
assert!(g2 > g1, "apply_update must bump the generation");
cache.apply_updates(&[]);
assert_eq!(cache.snapshot_generation(), g2);
let change = cache.modify_slot(addr, U256::from(1), |v| {
Some(v.unwrap_or_default() + U256::from(1))
});
assert!(change.is_some());
let g3 = cache.snapshot_generation();
assert!(g3 > g2, "modify_slot must bump the generation");
cache.inject_storage_batch(&[(addr, U256::from(9), U256::from(90))]);
assert_eq!(
cache.snapshot_generation(),
g3,
"inject_storage_batch is prefetch, not mutation"
);
cache.set_block(BlockId::Number(BlockNumberOrTag::Number(5)));
let g4 = cache.snapshot_generation();
assert!(g4 > g3, "set_block to a new pin must bump the generation");
cache.set_block(BlockId::Number(BlockNumberOrTag::Number(5)));
assert_eq!(
cache.snapshot_generation(),
g4,
"re-pinning to the same block is not a mutation"
);
let header = alloy_consensus::Header::default();
cache.advance_block(&header).expect("lenient advance");
assert!(cache.snapshot_generation() > g4);
}
#[test]
fn test_address_to_u256_conversion() {
let addr = Address::repeat_byte(0xAB);
let value = U256::from_be_slice(addr.as_slice());
let bytes = value.to_be_bytes::<32>();
assert_eq!(&bytes[..12], &[0u8; 12]);
assert_eq!(&bytes[12..], addr.as_slice());
}
#[test]
fn new_defaults_to_latest_block_pin() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let cache = rt.block_on(EvmCache::new(Arc::new(provider)));
assert_eq!(
cache.block(),
BlockId::latest(),
"a default cache must carry an explicit latest block pin, not None"
);
}
#[test]
fn test_set_block_context_stores_values() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
assert_eq!(cache.block_number(), None);
assert_eq!(cache.basefee(), None);
cache.set_block_context(Some(148_252_680), Some(50));
assert_eq!(cache.block_number(), Some(148_252_680));
assert_eq!(cache.basefee(), Some(50));
cache.set_block_context(None, None);
assert_eq!(cache.block_number(), None);
assert_eq!(cache.basefee(), None);
}
#[test]
fn set_block_latest_clears_stale_block_context() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
cache.set_block_context(Some(148_252_680), Some(50));
cache.set_block(BlockId::latest());
assert_eq!(
cache.block_number(),
None,
"tag pins must not retain a stale NUMBER context"
);
assert_eq!(
cache.basefee(),
None,
"set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
);
}
#[test]
fn set_block_latest_clears_stale_context_even_when_pin_unchanged() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
cache.set_block_context(Some(148_252_680), Some(50));
cache.set_block(BlockId::latest());
assert_eq!(
cache.block_number(),
None,
"latest pins must not retain a stale NUMBER context"
);
assert_eq!(
cache.basefee(),
None,
"latest pins can drift like tags, so stale BASEFEE must be cleared"
);
}
#[test]
fn set_block_number_sets_number_and_clears_stale_basefee() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
cache.set_block_context(Some(100), Some(50));
cache.set_block(BlockId::Number(BlockNumberOrTag::Number(200)));
assert_eq!(cache.block_number(), Some(200));
assert_eq!(
cache.basefee(),
None,
"set_block cannot refresh BASEFEE synchronously, so it must clear stale values"
);
}
#[test]
fn repin_to_block_clears_stale_basefee() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
cache.set_block_context(Some(100), Some(50));
cache.repin_to_block(200);
assert_eq!(cache.block_number(), Some(200));
assert_eq!(
cache.basefee(),
None,
"repin_to_block must not carry stale BASEFEE across blocks"
);
}
#[test]
fn test_build_evm_applies_block_context() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let mut cache = rt.block_on(EvmCache::new(Arc::new(provider)));
let block_num = 148_252_680u64;
let basefee_val = 50u64;
let coinbase = Address::repeat_byte(0xC0);
let prevrandao = B256::repeat_byte(0x77);
let gas_limit = 30_000_000u64;
cache.set_block_context(Some(block_num), Some(basefee_val));
cache.set_coinbase(Some(coinbase));
cache.set_prevrandao(Some(prevrandao));
cache.set_block_gas_limit(Some(gas_limit));
let evm = cache.build_evm();
assert_eq!(evm.block.number, U256::from(block_num));
assert_eq!(evm.block.basefee, basefee_val);
assert_eq!(evm.block.beneficiary, coinbase);
assert_eq!(evm.block.prevrandao, Some(prevrandao));
assert_eq!(evm.block.gas_limit, gas_limit);
}
#[test]
fn test_from_backend_propagates_block_context() {
use alloy_provider::RootProvider;
use alloy_rpc_client::RpcClient;
use alloy_transport::mock::Asserter;
let asserter = Asserter::new();
let client = RpcClient::mocked(asserter);
let provider = RootProvider::<AnyNetwork>::new(client);
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let parent = rt.block_on(EvmCache::new(Arc::new(provider)));
let block_num = Some(148_252_680u64);
let basefee_val = Some(50u64);
let child = EvmCache::from_backend(
parent.unchecked_backend().clone(),
parent.unchecked_blockchain_db().clone(),
parent.block(),
42161,
block_num,
basefee_val,
SpecId::CANCUN,
);
assert_eq!(child.block_number(), block_num);
assert_eq!(child.basefee(), basefee_val);
}
#[test]
fn unix_timestamp_secs_saturating_handles_pre_epoch() {
let before_epoch = std::time::UNIX_EPOCH - std::time::Duration::from_secs(5);
assert_eq!(
unix_timestamp_secs_saturating(before_epoch),
0,
"pre-epoch system times must saturate instead of panicking"
);
}
}