mod common;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use alloy_eips::BlockId;
use alloy_primitives::{Address, Bytes, U256};
use alloy_sol_types::SolCall;
use anyhow::Result;
use common::{
Gate, MOCK_ERC20_BALANCE_SLOT, MockERC20, failing_fetcher, gated_tracking_fetcher,
install_default_account, install_mock_erc20, panicking_fetcher, setup_cache, stub_fetcher,
};
use evm_fork_cache::cache::{
EvmCache, EvmOverlay, SimStatus, SlotObservationTracker, StorageBatchFetchFn,
};
use evm_fork_cache::errors::StorageFetchResult;
use evm_fork_cache::freshness::{
AlwaysVerify, BlockClock, FreshnessController, FreshnessParams, FreshnessRegistry, NeverVerify,
ObservationDriven, SimRequest, Validation, WallClock,
};
use revm::state::{AccountInfo, Bytecode};
const BLOCKHASH_READER_RUNTIME: &[u8] = &[
0x60, 0x00, 0x40, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xF3,
];
fn balance_slot_for(owner: Address) -> U256 {
use alloy_sol_types::SolValue;
let key =
alloy_primitives::keccak256((owner, U256::from(MOCK_ERC20_BALANCE_SLOT)).abi_encode());
U256::from_be_bytes(key.0)
}
fn transfer_calldata(to: Address, amount: U256) -> Bytes {
Bytes::from(MockERC20::transferCall { to, amount }.abi_encode())
}
fn balance_of_calldata(account: Address) -> Bytes {
Bytes::from(MockERC20::balanceOfCall { account }.abi_encode())
}
fn decode_balance(output: &Bytes) -> U256 {
MockERC20::balanceOfCall::abi_decode_returns(output).expect("decode balanceOf return")
}
async fn settle() {
tokio::task::yield_now().await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
tokio::task::yield_now().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn verify_slots_detects_and_injects_changes() -> Result<()> {
let mut cache = setup_cache().await?;
let contract = Address::repeat_byte(0x11);
install_mock_erc20(&mut cache, contract);
let slot_a = U256::from(10);
let slot_b = U256::from(20);
cache
.db_mut()
.insert_account_storage(contract, slot_a, U256::from(100))?;
cache
.db_mut()
.insert_account_storage(contract, slot_b, U256::from(200))?;
let values = HashMap::from([
((contract, slot_a), U256::from(999)),
((contract, slot_b), U256::from(200)),
]);
cache.set_storage_batch_fetcher(stub_fetcher(values));
let changed = cache.verify_slots(&[(contract, slot_a), (contract, slot_b)])?;
assert_eq!(changed.len(), 1, "only slot_a changed");
let change = &changed[0];
assert_eq!(change.address, contract);
assert_eq!(change.slot, slot_a);
assert_eq!(change.old, U256::from(100));
assert_eq!(change.new, U256::from(999));
assert_eq!(
cache.cached_storage_value(contract, slot_a),
Some(U256::from(999))
);
assert_eq!(
cache.cached_storage_value(contract, slot_b),
Some(U256::from(200))
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn verify_slots_unchanged_returns_empty() -> Result<()> {
let mut cache = setup_cache().await?;
let contract = Address::repeat_byte(0x22);
install_mock_erc20(&mut cache, contract);
let slot = U256::from(7);
cache
.db_mut()
.insert_account_storage(contract, slot, U256::from(42))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(contract, slot),
U256::from(42),
)])));
let changed = cache.verify_slots(&[(contract, slot)])?;
assert!(changed.is_empty(), "no change should be reported");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn verify_slots_treats_unseen_slot_as_zero() -> Result<()> {
let mut cache = setup_cache().await?;
let contract = Address::repeat_byte(0x33);
install_mock_erc20(&mut cache, contract);
let slot = U256::from(5);
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(contract, slot),
U256::from(77),
)])));
let changed = cache.verify_slots(&[(contract, slot)])?;
assert_eq!(changed.len(), 1);
assert_eq!(changed[0].old, U256::ZERO);
assert_eq!(changed[0].new, U256::from(77));
assert_eq!(
cache.cached_storage_value(contract, slot),
Some(U256::from(77))
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn verify_slots_skips_failed_fetches() -> Result<()> {
let mut cache = setup_cache().await?;
cache.set_storage_batch_fetcher(failing_fetcher());
let contract = Address::repeat_byte(0x44);
cache.inject_storage_batch(&[(contract, U256::from(1), U256::from(5))]);
let changed = cache.verify_slots(&[(contract, U256::from(1))])?;
assert!(
changed.is_empty(),
"failed fetches are skipped, not changes"
);
assert_eq!(
cache.cached_storage_value(contract, U256::from(1)),
Some(U256::from(5))
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn purge_account_drops_account_and_storage_from_both_layers() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x55);
let owner = Address::repeat_byte(0x66);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
cache.insert_mapping_storage_slot(
token,
U256::from(MOCK_ERC20_BALANCE_SLOT),
owner,
U256::from(1000),
)?;
let _ = common::balance_of(&mut cache, token, owner)?;
assert!(
cache.cache_db_storage_slot_count(token) > 0,
"overlay populated"
);
cache.inject_storage_batch(&[(token, U256::from(99), U256::from(1))]);
assert!(
cache.contract_storage_slot_count(token) > 0,
"backend populated"
);
assert!(
cache.db_mut().cache.accounts.contains_key(&token),
"overlay account present before purge"
);
cache.purge_account(token);
assert!(
!cache.db_mut().cache.accounts.contains_key(&token),
"overlay account removed"
);
assert_eq!(
cache.cache_db_storage_slot_count(token),
0,
"overlay storage gone"
);
assert_eq!(
cache.contract_storage_slot_count(token),
0,
"backend storage gone"
);
{
let accounts = cache.unchecked_blockchain_db().accounts().read();
assert!(!accounts.contains_key(&token), "backend account removed");
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn overlay_call_raw_with_access_list_captures_read_set() -> Result<()> {
let mut cache = setup_cache().await?;
let token = Address::repeat_byte(0x77);
let owner = Address::repeat_byte(0x88);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let balance_slot = U256::from(MOCK_ERC20_BALANCE_SLOT);
cache.insert_mapping_storage_slot(token, balance_slot, owner, U256::from(1000))?;
let snapshot = cache.snapshot();
let mut overlay = EvmOverlay::new(Arc::clone(&snapshot), None);
let call = common::MockERC20::balanceOfCall { account: owner };
let (result, access) =
overlay.call_raw_with_access_list(owner, token, Bytes::from(call.abi_encode()))?;
assert!(result.is_success(), "balanceOf should succeed: {result:?}");
assert!(access.accounts.contains(&token), "token account touched");
let hashed = {
use alloy_sol_types::SolValue;
let key = alloy_primitives::keccak256((owner, balance_slot).abi_encode());
U256::from_be_bytes(key.0)
};
assert!(
access.slots.contains(&(token, hashed)),
"balance mapping slot captured in read set"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn overlay_override_slot_takes_precedence() -> Result<()> {
let mut cache = setup_cache().await?;
let contract = Address::repeat_byte(0x99);
install_mock_erc20(&mut cache, contract);
let slot = U256::from(3);
cache.inject_storage_batch(&[(contract, slot, U256::from(1))]);
let snapshot = cache.snapshot();
let mut overlay = EvmOverlay::new(snapshot, None);
overlay.override_slot(contract, slot, U256::from(999));
use revm::database_interface::Database;
assert_eq!(overlay.storage(contract, slot)?, U256::from(999));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn cache_has_fetcher_over_mock_provider() -> Result<()> {
let cache: EvmCache = setup_cache().await?;
assert!(
cache.storage_batch_fetcher().is_some(),
"mock-provider cache has a fetcher"
);
Ok(())
}
async fn cache_with_balance(token: Address, owner: Address, balance: U256) -> Result<EvmCache> {
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
if balance > U256::ZERO {
cache
.db_mut()
.insert_account_storage(token, balance_slot_for(owner), balance)?;
}
Ok(cache)
}
#[tokio::test(flavor = "multi_thread")]
async fn run_match_path_confirmed() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(1000),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req = SimRequest::new(owner, token, transfer_calldata(recipient, U256::from(100)));
let sim = controller.run(&mut cache, vec![req])?;
assert_eq!(sim.optimistic().len(), 1);
let optimistic_gas = sim.optimistic()[0].gas_used;
assert!(optimistic_gas > 0);
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::ConfirmedStorage),
"unchanged values should confirm: {validation:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_blockhash_reading_sim_fails_closed_as_unverified() -> Result<()> {
let caller = Address::repeat_byte(0x0c);
let target = Address::repeat_byte(0x0d);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, caller);
let bytecode = Bytecode::new_raw(Bytes::from_static(BLOCKHASH_READER_RUNTIME));
let code_hash = bytecode.hash_slow();
cache.db_mut().insert_account_info(
target,
AccountInfo {
balance: U256::ZERO,
nonce: 0,
code: Some(bytecode),
code_hash,
account_id: None,
},
);
cache
.db_mut()
.replace_account_storage(target, Default::default())?;
cache.set_block(BlockId::number(100));
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::new()));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(caller, target, Bytes::new())],
)?;
assert_eq!(
sim.optimistic().len(),
1,
"the optimistic run still executes"
);
let validation = sim.validate().await?;
match validation {
Validation::Unverified { reason } => {
assert!(
reason.contains("BLOCKHASH"),
"the reason must name the unverifiable read: {reason}"
);
}
other => panic!("BLOCKHASH-reading sim must fail closed, got {other:?}"),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_mismatch_path_corrected_only_affected_rerun() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let token2 = Address::repeat_byte(0x77);
let owner2 = Address::repeat_byte(0x88);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_default_account(&mut cache, owner2);
install_mock_erc20(&mut cache, token);
install_mock_erc20(&mut cache, token2);
cache
.db_mut()
.insert_account_storage(token, balance_slot_for(owner), U256::from(1000))?;
cache
.db_mut()
.insert_account_storage(token2, balance_slot_for(owner2), U256::from(5000))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([
((token, balance_slot_for(owner)), U256::from(50)),
((token2, balance_slot_for(owner2)), U256::from(5000)),
])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req1 = SimRequest::new(owner, token, transfer_calldata(recipient, U256::from(100)));
let req2 = SimRequest::new(
owner2,
token2,
transfer_calldata(recipient, U256::from(100)),
);
let sim = controller.run(&mut cache, vec![req1, req2])?;
let opt = sim.optimistic().to_vec();
assert_eq!(opt.len(), 2);
assert!(
!opt[0].logs.is_empty(),
"req1 optimistic should succeed (a log)"
);
assert!(
!opt[1].logs.is_empty(),
"req2 optimistic should succeed (a log)"
);
assert!(
opt[0].token_deltas.is_empty(),
"optimistic token_deltas are empty (no transfer tracking)"
);
assert!(
opt[1].token_deltas.is_empty(),
"optimistic token_deltas empty"
);
let validation = sim.validate().await?;
match validation {
Validation::Corrected {
results,
changed_slots,
..
} => {
assert_eq!(
changed_slots.len(),
1,
"only owner's balance changed: {changed_slots:?}"
);
assert_eq!(changed_slots[0].address, token);
assert_eq!(changed_slots[0].slot, balance_slot_for(owner));
assert_eq!(changed_slots[0].old, U256::from(1000));
assert_eq!(changed_slots[0].new, U256::from(50));
assert!(
results[0].logs.is_empty(),
"corrected req1 should now revert and emit no log"
);
assert_ne!(
results[0].gas_used, opt[0].gas_used,
"corrected req1 gas should differ from the optimistic success"
);
assert_eq!(results[1].gas_used, opt[1].gas_used, "req2 not re-run");
assert_eq!(results[1].logs.len(), opt[1].logs.len(), "req2 unchanged");
assert!(
results[0].token_deltas.is_empty(),
"corrected result token_deltas are empty (no transfer tracking)"
);
}
other => panic!("expected Corrected, got {other:?}"),
}
assert_eq!(
controller.rerun_count(),
1,
"only the affected sim (req1) should be re-run, not req2"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_view_call_corrected_success_to_different_success() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(250),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req = SimRequest::new(owner, token, balance_of_calldata(owner));
let sim = controller.run(&mut cache, vec![req])?;
let opt = sim.optimistic().to_vec();
assert_eq!(opt.len(), 1);
assert!(!opt[0].output.is_empty(), "view call returns data");
assert_eq!(
decode_balance(&opt[0].output),
U256::from(1000),
"optimistic returns the old balance"
);
let validation = sim.validate().await?;
match validation {
Validation::Corrected {
results,
changed_slots,
..
} => {
assert_eq!(changed_slots.len(), 1, "exactly the balance slot changed");
assert_eq!(changed_slots[0].address, token);
assert_eq!(changed_slots[0].slot, balance_slot_for(owner));
assert_eq!(changed_slots[0].old, U256::from(1000));
assert_eq!(changed_slots[0].new, U256::from(250));
assert_eq!(
decode_balance(&results[0].output),
U256::from(250),
"corrected re-run returns the new balance"
);
assert!(
!results[0].output.is_empty(),
"corrected run still succeeds"
);
assert_ne!(
results[0].output, opt[0].output,
"corrected output differs from optimistic output"
);
}
other => panic!("expected Corrected, got {other:?}"),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_drains_pending_on_next_run() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
cache
.db_mut()
.insert_account_storage(token, balance_slot_for(owner), U256::from(1000))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(2000),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let validation = sim.validate().await?;
assert!(matches!(validation, Validation::Corrected { .. }));
assert_eq!(controller.pending_len(), 1, "a correction was queued");
assert_eq!(
cache.cached_storage_value(token, balance_slot_for(owner)),
Some(U256::from(1000))
);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert_eq!(controller.pending_len(), 0, "pending drained");
assert_eq!(
cache.cached_storage_value(token, balance_slot_for(owner)),
Some(U256::from(2000)),
"correction applied to the live cache"
);
assert!(
!sim.optimistic()[0].logs.is_empty(),
"optimistic still succeeds"
);
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::ConfirmedStorage),
"{validation:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_converges_when_corrected_slot_is_overlay_resident() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let slot = balance_slot_for(owner);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(1000))?;
assert_eq!(
cache.cached_storage_value(token, slot),
Some(U256::from(1000)),
"precondition: overlay holds the seeded value"
);
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, slot),
U256::from(2000),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert!(matches!(
sim.validate().await?,
Validation::Corrected { .. }
));
assert_eq!(controller.pending_len(), 1);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert_eq!(controller.pending_len(), 0, "pending drained");
assert_eq!(
cache.cached_storage_value(token, slot),
Some(U256::from(2000)),
"correction must overwrite the overlay-resident slot, not just layer 2"
);
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::ConfirmedStorage),
"must converge, got {validation:?}"
);
assert_eq!(controller.rerun_count(), 1, "only the first cycle re-ran");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn verify_slots_heals_overlay_resident_slot() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO); install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let slot = balance_slot_for(owner);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(100))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, slot),
U256::from(999),
)])));
let changed = cache.verify_slots(&[(token, slot)])?;
assert_eq!(changed.len(), 1, "stale overlay slot detected as changed");
assert_eq!(
cache.cached_storage_value(token, slot),
Some(U256::from(999)),
"verify_slots heals the overlay-resident slot"
);
let balance = common::balance_of(&mut cache, token, owner)?;
assert_eq!(
balance,
U256::from(999),
"EVM SLOAD reflects the healed overlay slot"
);
assert!(
cache.verify_slots(&[(token, slot)])?.is_empty(),
"overlay slot healed; re-verify is idempotent"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn optimistic_result_reports_status_per_outcome() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let slot = balance_slot_for(owner);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(1000))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, slot),
U256::from(1000),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(owner, token, balance_of_calldata(owner))],
)?;
assert_eq!(sim.optimistic()[0].status, SimStatus::Success);
assert!(
sim.optimistic()[0].logs.is_empty(),
"the view call emits no logs"
);
assert_eq!(
decode_balance(&sim.optimistic()[0].output),
U256::from(1000)
);
sim.into_optimistic();
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(5000)),
)],
)?;
assert_eq!(sim.optimistic()[0].status, SimStatus::Revert);
sim.into_optimistic();
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn dropping_after_fetch_started_suppresses_correction() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let slot = balance_slot_for(owner);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(1000))?;
let barrier = Arc::new(std::sync::Barrier::new(2));
let fb = Arc::clone(&barrier);
let fetcher: StorageBatchFetchFn =
Arc::new(move |reqs: Vec<(Address, U256)>, _block: BlockId| {
fb.wait(); fb.wait(); reqs.into_iter()
.map(|(a, s)| (a, s, Ok(U256::from(2000))))
.collect()
});
cache.set_storage_batch_fetcher(fetcher);
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
barrier.wait(); drop(sim); barrier.wait();
settle().await;
assert_eq!(
controller.pending_len(),
0,
"a cancel observed after the fetch must suppress the queued correction"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_corrected_rerun_verifies_newly_read_volatile_slot() -> Result<()> {
use revm::state::{AccountInfo, Bytecode};
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
let caller = Address::repeat_byte(0x66);
install_default_account(&mut cache, caller);
let contract = Address::repeat_byte(0x55);
let code = Bytecode::new_raw(Bytes::from(
alloy_primitives::hex::decode("600054806013575060015460005260206000f35b60005260206000f3")
.expect("valid runtime hex"),
));
let code_hash = code.hash_slow();
cache.db_mut().insert_account_info(
contract,
AccountInfo {
balance: U256::ZERO,
nonce: 0,
code: Some(code),
code_hash,
account_id: None,
},
);
cache
.db_mut()
.replace_account_storage(contract, Default::default())
.unwrap();
let slot_a = U256::from(0);
let slot_b = U256::from(1);
cache
.db_mut()
.insert_account_storage(contract, slot_a, U256::from(5))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([
((contract, slot_a), U256::from(0)),
((contract, slot_b), U256::from(777)),
])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(caller, contract, Bytes::new())],
)?;
assert_eq!(
U256::from_be_slice(&sim.optimistic()[0].output),
U256::from(5)
);
match sim.validate().await? {
Validation::Corrected {
results,
changed_slots,
..
} => {
let keys: std::collections::HashSet<(Address, U256)> =
changed_slots.iter().map(|c| (c.address, c.slot)).collect();
assert!(keys.contains(&(contract, slot_a)), "A reported as changed");
assert!(
keys.contains(&(contract, slot_b)),
"B (read only on the corrected branch) must be verified and reported"
);
assert_eq!(
U256::from_be_slice(&results[0].output),
U256::from(777),
"corrected result must use the FRESH value of the newly-read slot, not stale 0"
);
}
other => panic!("expected Corrected, got {other:?}"),
}
assert_eq!(controller.rerun_count(), 1);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn overlay_call_with_tx_config_threads_value() -> Result<()> {
use evm_fork_cache::cache::TxConfig;
use revm::context::result::ExecutionResult;
use revm::state::{AccountInfo, Bytecode};
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
let caller = Address::repeat_byte(0x66);
install_default_account(&mut cache, caller);
let callee = Address::repeat_byte(0x55);
let code = Bytecode::new_raw(Bytes::from(
alloy_primitives::hex::decode("3460005260206000f3").expect("valid runtime hex"),
));
let code_hash = code.hash_slow();
cache.db_mut().insert_account_info(
callee,
AccountInfo {
balance: U256::ZERO,
nonce: 0,
code: Some(code),
code_hash,
account_id: None,
},
);
cache
.db_mut()
.replace_account_storage(callee, Default::default())
.unwrap();
let snapshot = cache.snapshot();
let mut overlay = EvmOverlay::new(snapshot, None);
fn returned_value(res: ExecutionResult) -> U256 {
match res {
ExecutionResult::Success { output, .. } => U256::from_be_slice(&output.into_data()),
other => panic!("expected success, got {other:?}"),
}
}
let (res, _) = overlay.call_raw_with_access_list(caller, callee, Bytes::new())?;
assert_eq!(returned_value(res), U256::ZERO);
let tx = TxConfig {
value: U256::from(12_345u64),
..Default::default()
};
let (res, _) = overlay.call_raw_with_access_list_with(caller, callee, Bytes::new(), &tx)?;
assert_eq!(returned_value(res), U256::from(12_345u64));
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_honors_tx_gas_limit() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let slot = balance_slot_for(owner);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(1000))?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, slot),
U256::from(1000),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req = SimRequest::new(owner, token, transfer_calldata(recipient, U256::from(100)))
.with_gas_limit(30_000);
let sim = controller.run(&mut cache, vec![req])?;
assert!(
matches!(sim.optimistic()[0].status, SimStatus::Halt { .. }),
"gas-bounded transfer must halt, got {:?}",
sim.optimistic()[0].status
);
sim.into_optimistic();
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn validator_fetches_at_captured_latest_pin_despite_repin() -> Result<()> {
use alloy_eips::BlockNumberOrTag;
let token = Address::repeat_byte(0x91);
let owner = Address::repeat_byte(0x92);
let recipient = Address::repeat_byte(0x93);
let slot = balance_slot_for(owner);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(1000))?;
assert_eq!(
cache.block(),
BlockId::latest(),
"default construction must expose an explicit latest pin"
);
let barrier = Arc::new(std::sync::Barrier::new(2));
let fb = Arc::clone(&barrier);
let seen_block: Arc<Mutex<Option<BlockId>>> = Arc::new(Mutex::new(None));
let seen = Arc::clone(&seen_block);
let fetcher: StorageBatchFetchFn =
Arc::new(move |reqs: Vec<(Address, U256)>, block: BlockId| {
*seen.lock().unwrap() = Some(block);
fb.wait();
fb.wait();
let at_snapshot_pin = block == BlockId::latest();
reqs.into_iter()
.map(|(a, s)| {
let v = if s == slot {
if at_snapshot_pin {
U256::from(1000)
} else {
U256::from(2000)
}
} else {
U256::ZERO
};
(a, s, Ok(v))
})
.collect()
});
cache.set_storage_batch_fetcher(fetcher);
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
barrier.wait();
cache.set_block(BlockId::Number(BlockNumberOrTag::Number(101)));
barrier.wait();
let verdict = sim.validate().await?;
assert!(
matches!(verdict, Validation::ConfirmedStorage),
"validator must fetch at the captured latest pin, not the later numeric repin; got {verdict:?}"
);
assert_eq!(
*seen_block.lock().unwrap(),
Some(BlockId::latest()),
"the fetch must receive the concrete snapshot pin"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn validator_fetches_at_snapshot_block_despite_repin() -> Result<()> {
use alloy_eips::BlockNumberOrTag;
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let slot = balance_slot_for(owner);
let n = 100u64;
let block_n = BlockId::Number(BlockNumberOrTag::Number(n));
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
cache.set_block(block_n);
cache
.db_mut()
.insert_account_storage(token, slot, U256::from(1000))?;
assert_eq!(
cache.cached_storage_value(token, slot),
Some(U256::from(1000)),
"PRECONDITION: seeded balance present after set_block + insert"
);
let barrier = Arc::new(std::sync::Barrier::new(2));
let fb = Arc::clone(&barrier);
let seen_block: Arc<Mutex<Option<BlockId>>> = Arc::new(Mutex::new(None));
let seen = Arc::clone(&seen_block);
let fetcher: StorageBatchFetchFn =
Arc::new(move |reqs: Vec<(Address, U256)>, block: BlockId| {
*seen.lock().unwrap() = Some(block);
fb.wait(); fb.wait(); let at_n = block == block_n;
reqs.into_iter()
.map(|(a, s)| {
let v = if s == slot {
if at_n {
U256::from(1000)
} else {
U256::from(2000)
}
} else {
U256::ZERO
};
(a, s, Ok(v))
})
.collect()
});
cache.set_storage_batch_fetcher(fetcher);
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
barrier.wait(); cache.set_block(BlockId::Number(BlockNumberOrTag::Number(n + 1)));
barrier.wait();
let verdict = sim.validate().await?;
assert!(
matches!(verdict, Validation::ConfirmedStorage),
"validator must fetch at the snapshot's block N, not the re-pinned N+1; got {verdict:?}"
);
assert_eq!(
*seen_block.lock().unwrap(),
Some(block_n),
"the fetch must be pinned to the snapshot block N"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_unverified_on_fetcher_error() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(failing_fetcher());
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::Unverified { .. }),
"fetcher error should yield Unverified: {validation:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_into_optimistic_aborts_validation() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
let gate = Gate::new();
cache.set_storage_batch_fetcher(gated_tracking_fetcher(
HashMap::from([((token, balance_slot_for(owner)), U256::from(50))]),
gate.clone(),
));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let results = sim.into_optimistic(); gate.release();
assert_eq!(results.len(), 1);
settle().await;
assert_eq!(
controller.pending_len(),
0,
"into_optimistic must abort validation before it queues a correction"
);
assert_eq!(controller.rerun_count(), 0, "no re-run after abort");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn dropping_speculative_sim_aborts_before_queueing_correction() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
let gate = Gate::new();
cache.set_storage_batch_fetcher(gated_tracking_fetcher(
HashMap::from([((token, balance_slot_for(owner)), U256::from(50))]),
gate.clone(),
));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
drop(sim);
gate.release();
settle().await;
assert_eq!(
controller.pending_len(),
0,
"dropping the sim must abort validation before it queues a correction"
);
assert_eq!(controller.rerun_count(), 0, "no re-run after abort");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn never_verify_skips_predicted_but_reconciles_read_set() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(50),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), NeverVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::Corrected { .. }),
"actual-read-set reconcile should still catch the change: {validation:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn pinned_slot_is_not_verified() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(9999), )])));
let mut registry = FreshnessRegistry::new();
registry.pin_slot(token, balance_slot_for(owner));
let mut controller = FreshnessController::new(registry, AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::ConfirmedStorage),
"pinned slot must not be verified: {validation:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn wall_clock_controller_runs() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(1000),
)])));
let mut controller =
FreshnessController::with_clock(FreshnessRegistry::new(), AlwaysVerify, WallClock);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::ConfirmedStorage),
"{validation:?}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn valid_through_becomes_volatile_after_boundary() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(2000), )])));
let mut registry = FreshnessRegistry::new();
registry.valid_through_slot(token, balance_slot_for(owner), 100);
let clock = BlockClock::at(100);
let mut controller = FreshnessController::with_clock(registry, AlwaysVerify, clock.clone());
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert!(matches!(
sim.validate().await?,
Validation::ConfirmedStorage
));
clock.set_block(101);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert!(
matches!(sim.validate().await?, Validation::Corrected { .. }),
"past ValidThrough boundary the slot is volatile and the change is caught"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_missing_slot_treated_as_zero_is_corrected() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
let slot = balance_slot_for(owner);
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, slot),
U256::from(777),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req = SimRequest::new(owner, token, balance_of_calldata(owner));
let sim = controller.run(&mut cache, vec![req])?;
let opt = sim.optimistic().to_vec();
assert_eq!(
decode_balance(&opt[0].output),
U256::ZERO,
"unseen slot reads as zero optimistically"
);
match sim.validate().await? {
Validation::Corrected {
results,
changed_slots,
..
} => {
let change = changed_slots
.iter()
.find(|c| c.address == token && c.slot == slot)
.expect("the missing balance slot should be reported as changed");
assert_eq!(change.old, U256::ZERO, "missing slot treated as old = zero");
assert_eq!(change.new, U256::from(777), "fetcher's nonzero value");
assert_eq!(
decode_balance(&results[0].output),
U256::from(777),
"corrected re-run returns the fresh balance"
);
}
other => panic!("expected Corrected, got {other:?}"),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn pending_drain_alters_subsequent_result() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(50),
)])));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert!(
!sim.optimistic()[0].logs.is_empty(),
"first-run optimistic transfer succeeds against cached 1000"
);
assert!(matches!(
sim.validate().await?,
Validation::Corrected { .. }
));
assert_eq!(controller.pending_len(), 1, "a correction (→50) is queued");
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert_eq!(controller.pending_len(), 0, "pending drained");
assert!(
sim.optimistic()[0].logs.is_empty(),
"second-run optimistic transfer REVERTS — the drained value (50 < 100) \
changed the *result*, not just the cached value"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn validate_returns_err_on_fetcher_panic() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(panicking_fetcher());
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
let err = sim
.validate()
.await
.expect_err("a panicking fetcher should make validate return Err");
assert!(
err.to_string().contains("validation task failed"),
"a panicking fetcher should surface the JoinError: {err}"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_unverified_without_fetcher() -> Result<()> {
use revm::primitives::hardfork::SpecId;
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let base = cache_with_balance(token, owner, U256::from(1000)).await?;
let mut cache = EvmCache::from_backend(
base.unchecked_backend().clone(),
base.unchecked_blockchain_db().clone(),
base.block(),
base.chain_id(),
None,
None,
SpecId::CANCUN,
);
install_default_account(&mut cache, Address::ZERO);
install_default_account(&mut cache, owner);
install_mock_erc20(&mut cache, token);
cache.inject_storage_batch(&[(token, balance_slot_for(owner), U256::from(1000))]);
assert!(
cache.storage_batch_fetcher().is_none(),
"from_backend cache has no fetcher"
);
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
match sim.validate().await? {
Validation::Unverified { reason } => {
assert_eq!(reason, "no storage batch fetcher available", "{reason}");
}
other => panic!("expected Unverified, got {other:?}"),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn observation_driven_controller_end_to_end() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(50), )])));
let params = FreshnessParams::default();
let slot = balance_slot_for(owner);
let tracker = {
let mut t = SlotObservationTracker::new();
for now in 0..params.min_observations {
t.observe(token, slot, U256::from(1000), now as u64);
}
assert!(!t.should_refetch(token, slot, params.min_observations as u64, ¶ms));
Arc::new(Mutex::new(t))
};
use alloy_eips::eip2930::{AccessList, AccessListItem};
let predicted = AccessList(vec![AccessListItem {
address: token,
storage_keys: vec![alloy_primitives::B256::from(slot)],
}]);
let clock = BlockClock::at(params.min_observations as u64);
let mut controller = FreshnessController::with_clock(
FreshnessRegistry::new(),
ObservationDriven::new(params),
clock,
)
.with_tracker(Arc::clone(&tracker));
let req = SimRequest::new(owner, token, transfer_calldata(recipient, U256::from(100)))
.with_access_list(predicted);
let sim = controller.run(&mut cache, vec![req])?;
match sim.validate().await? {
Validation::Corrected { changed_slots, .. } => {
assert!(
changed_slots
.iter()
.any(|c| c.address == token && c.slot == slot),
"the actual-read-set reconcile catches the balance change"
);
}
other => panic!("expected Corrected, got {other:?}"),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn on_new_block_ages_valid_through() -> Result<()> {
let token = Address::repeat_byte(0x44);
let owner = Address::repeat_byte(0x55);
let recipient = Address::repeat_byte(0x66);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::from([(
(token, balance_slot_for(owner)),
U256::from(2000), )])));
let mut registry = FreshnessRegistry::new();
registry.valid_through_slot(token, balance_slot_for(owner), 100);
let mut controller =
FreshnessController::with_clock(registry, AlwaysVerify, BlockClock::at(100));
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert!(
matches!(sim.validate().await?, Validation::ConfirmedStorage),
"at block 100 the ValidThrough slot is still pinned"
);
controller.on_new_block(101);
let sim = controller.run(
&mut cache,
vec![SimRequest::new(
owner,
token,
transfer_calldata(recipient, U256::from(100)),
)],
)?;
assert!(
matches!(sim.validate().await?, Validation::Corrected { .. }),
"after on_new_block(101) the slot is volatile and the change is caught"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn run_unverified_when_fetcher_omits_requested_slot() -> Result<()> {
let token = Address::repeat_byte(0x11);
let owner = Address::repeat_byte(0x22);
let recipient = Address::repeat_byte(0x33);
let mut cache = cache_with_balance(token, owner, U256::from(1000)).await?;
cache.set_storage_batch_fetcher(Arc::new(|_req: Vec<(Address, U256)>, _block: BlockId| {
Vec::<(Address, U256, StorageFetchResult<U256>)>::new()
}));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req = SimRequest::new(owner, token, transfer_calldata(recipient, U256::from(100)));
let sim = controller.run(&mut cache, vec![req])?;
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::Unverified { .. }),
"a fetcher that omits a requested slot must yield Unverified, not a false \
confirmation/correction: {validation:?}"
);
assert_eq!(
controller.pending_len(),
0,
"Unverified must not queue any correction"
);
Ok(())
}
fn chained_sload_bytecode(n: u8) -> Bytes {
let ret_dest = 8u16 * (n as u16) + 2; assert!(ret_dest <= 255, "return dest must fit in PUSH1");
let ret = ret_dest as u8;
let mut code = Vec::new();
for i in 0..n {
code.extend_from_slice(&[0x60, i, 0x54, 0x80, 0x60, ret, 0x57, 0x50]);
}
code.extend_from_slice(&[0x60, 0x00]); code.extend_from_slice(&[0x5b, 0x60, 0x00, 0x52, 0x60, 0x20, 0x60, 0x00, 0xf3]);
Bytes::from(code)
}
#[tokio::test(flavor = "multi_thread")]
async fn run_unverified_when_fixed_point_round_cap_exceeded() -> Result<()> {
use revm::state::{AccountInfo, Bytecode};
let mut cache = setup_cache().await?;
install_default_account(&mut cache, Address::ZERO);
let caller = Address::repeat_byte(0x66);
install_default_account(&mut cache, caller);
let contract = Address::repeat_byte(0x55);
let code = Bytecode::new_raw(chained_sload_bytecode(12));
let code_hash = code.hash_slow();
cache.db_mut().insert_account_info(
contract,
AccountInfo {
balance: U256::ZERO,
nonce: 0,
code: Some(code),
code_hash,
account_id: None,
},
);
cache
.db_mut()
.replace_account_storage(contract, Default::default())
.unwrap();
for i in 0..12u64 {
cache
.db_mut()
.insert_account_storage(contract, U256::from(i), U256::from(1))?;
}
cache.set_storage_batch_fetcher(stub_fetcher(HashMap::new()));
let mut controller = FreshnessController::new(FreshnessRegistry::new(), AlwaysVerify);
let req = SimRequest::new(caller, contract, Bytes::new());
let sim = controller.run(&mut cache, vec![req])?;
let validation = sim.validate().await?;
assert!(
matches!(validation, Validation::Unverified { .. }),
"exceeding the fixed-point round cap must yield Unverified, not a trusted \
Corrected: {validation:?}"
);
assert_eq!(
controller.pending_len(),
0,
"an Unverified (cap-exceeded) validation must queue no corrections"
);
Ok(())
}
#[test]
fn verdict_taxonomy_separates_storage_only_from_full_confirmation() {
assert!(matches!(
Validation::ConfirmedStorage,
Validation::ConfirmedStorage
));
assert!(matches!(
Validation::ConfirmedFull,
Validation::ConfirmedFull
));
let corrected = Validation::Corrected {
results: vec![],
changed_slots: vec![],
changed_accounts: vec![],
};
assert!(matches!(corrected, Validation::Corrected { .. }));
}