use alloy::primitives::map::AddressHashMap;
use alloy::primitives::B256;
use alloy::rpc::types::eth::AccessList;
use revm::bytecode::opcode::{SLOAD, SSTORE};
use revm::inspector::Inspector;
use revm::interpreter::interpreter_types::{InputsTr, Jumps, StackTr};
use revm::interpreter::{Interpreter, InterpreterTypes};
use revm::state::{Account, EvmState};
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::rc::Rc;
#[must_use]
pub fn emit_access_list_from_state(state: &EvmState) -> AccessList {
let items: Vec<alloy::rpc::types::eth::AccessListItem> = state
.iter()
.filter(|(_, account)| account.is_touched())
.filter_map(|(address, account)| {
let storage_keys: Vec<B256> = storage_keys_for(account);
if storage_keys.is_empty() {
None
} else {
Some(alloy::rpc::types::eth::AccessListItem {
address: *address,
storage_keys,
})
}
})
.collect();
AccessList::from(items)
}
fn storage_keys_for(account: &Account) -> Vec<B256> {
account.storage.keys().map(|key| B256::from(*key)).collect()
}
#[derive(Debug, Default)]
struct CollectedSlots {
touched: AddressHashMap<BTreeSet<B256>>,
}
#[derive(Debug, Clone)]
pub struct AccessListCollector {
slots: Rc<RefCell<CollectedSlots>>,
}
#[derive(Debug, Clone)]
pub struct AccessListHandle {
slots: Rc<RefCell<CollectedSlots>>,
}
impl Default for AccessListCollector {
fn default() -> Self {
Self {
slots: Rc::new(RefCell::new(CollectedSlots::default())),
}
}
}
impl AccessListCollector {
#[must_use]
pub fn new() -> (Self, AccessListHandle) {
let slots = Rc::new(RefCell::new(CollectedSlots::default()));
(
Self {
slots: Rc::clone(&slots),
},
AccessListHandle { slots },
)
}
}
impl AccessListHandle {
#[must_use]
pub fn take_access_list(&self) -> AccessList {
let touched = std::mem::take(&mut self.slots.borrow_mut().touched);
let items: Vec<alloy::rpc::types::eth::AccessListItem> = touched
.into_iter()
.filter_map(|(address, slots)| {
if slots.is_empty() {
None
} else {
Some(alloy::rpc::types::eth::AccessListItem {
address,
storage_keys: slots.into_iter().collect(),
})
}
})
.collect();
AccessList::from(items)
}
}
impl<CTX, INTR: InterpreterTypes> Inspector<CTX, INTR> for AccessListCollector {
fn step(&mut self, interp: &mut Interpreter<INTR>, _context: &mut CTX) {
let opcode = interp.bytecode.opcode();
if opcode == SLOAD || opcode == SSTORE {
let address = interp.input.target_address();
if let Some(&slot) = interp.stack.data().last() {
self.slots
.borrow_mut()
.touched
.entry(address)
.or_default()
.insert(B256::from(slot));
}
}
}
}
#[expect(clippy::expect_used)]
#[cfg(test)]
mod tests {
use super::*;
use alloy::primitives::{Address, Bytes, B256, U256};
use revm::state::{
Account, AccountStatus, EvmState, EvmStorage, EvmStorageSlot, TransactionId,
};
fn state_with_touched_storage(addr: Address, slots: &[U256]) -> EvmState {
let mut storage = EvmStorage::default();
for &slot in slots {
storage.insert(slot, EvmStorageSlot::new(slot, TransactionId::ZERO));
}
let mut account = Account::default();
account.storage = storage;
account.status = AccountStatus::Touched;
let mut state = EvmState::default();
state.insert(addr, account);
state
}
#[test]
fn emit_access_list_collects_touched_storage_slots() {
let addr = Address::ZERO;
let slot = U256::from(0x42u64);
let state = state_with_touched_storage(addr, &[slot]);
let access_list = emit_access_list_from_state(&state);
assert_eq!(access_list.len(), 1, "one touched account");
let item = &access_list[0];
assert_eq!(item.address, addr);
assert_eq!(item.storage_keys, vec![B256::from(slot)]);
}
#[test]
fn emit_access_list_skips_touched_account_with_no_storage() {
let addr = Address::ZERO;
let state = state_with_touched_storage(addr, &[]);
let access_list = emit_access_list_from_state(&state);
assert!(
access_list.is_empty(),
"balance-only touch contributes no slots"
);
}
#[test]
fn emit_access_list_skips_untouched_account() {
let addr = Address::ZERO;
let slot = U256::from(0x99u64);
let mut storage = EvmStorage::default();
storage.insert(slot, EvmStorageSlot::new(slot, TransactionId::ZERO));
let mut account = Account::default();
account.storage = storage;
account.status = AccountStatus::default();
let mut state = EvmState::default();
state.insert(addr, account);
let access_list = emit_access_list_from_state(&state);
assert!(access_list.is_empty(), "untouched account skipped");
}
#[test]
fn emit_access_list_groups_slots_per_address() {
let addr_a = Address::ZERO;
let addr_b = Address::repeat_byte(0x11);
let slot_first = U256::from(1u64);
let slot_second = U256::from(2u64);
let slot_third = U256::from(3u64);
let state_a = state_with_touched_storage(addr_a, &[slot_first, slot_second]);
let state_b = state_with_touched_storage(addr_b, &[slot_third]);
let mut state = state_a;
state.extend(state_b);
let access_list = emit_access_list_from_state(&state);
assert_eq!(access_list.len(), 2, "two touched accounts");
let item_a = access_list
.iter()
.find(|i| i.address == addr_a)
.expect("addr_a present");
assert_eq!(item_a.storage_keys.len(), 2);
assert!(item_a.storage_keys.contains(&B256::from(slot_first)));
assert!(item_a.storage_keys.contains(&B256::from(slot_second)));
let item_b = access_list
.iter()
.find(|i| i.address == addr_b)
.expect("addr_b present");
assert_eq!(item_b.storage_keys, vec![B256::from(slot_third)]);
}
#[test]
fn emit_access_list_empty_state_is_empty_list() {
let state = EvmState::default();
let access_list = emit_access_list_from_state(&state);
assert!(access_list.is_empty());
}
use revm::bytecode::opcode;
use revm::bytecode::Bytecode;
use revm::context::Context as RevmContext;
use revm::context::TxEnv;
use revm::database::CacheDB;
use revm::database_interface::EmptyDB;
use revm::primitives::TxKind;
use revm::state::AccountInfo;
use revm::{ExecuteEvm, InspectEvm, MainBuilder, MainContext};
fn fixture_db(contract_addr: Address, code: Bytecode) -> CacheDB<EmptyDB> {
let mut cache_db = CacheDB::new(EmptyDB::default());
cache_db.insert_account_info(
contract_addr,
AccountInfo {
balance: U256::from(1_000_000_000_000_000_000u64),
code: Some(code),
..Default::default()
},
);
cache_db
}
fn fixture_bytecode() -> Bytes {
alloy::primitives::Bytes::from(vec![
opcode::PUSH1,
0x01,
opcode::SLOAD, opcode::PUSH1,
0x99,
opcode::PUSH1,
0x02,
opcode::SSTORE, opcode::STOP,
])
}
#[test]
fn access_list_collector_matches_state_journal_emitter() {
let contract_addr = Address::repeat_byte(0x42);
let tx = TxEnv::builder()
.kind(TxKind::Call(contract_addr))
.gas_limit(100_000)
.build()
.expect("well-formed tx");
let db_a = fixture_db(contract_addr, Bytecode::new_raw(fixture_bytecode()));
let evm_a = RevmContext::mainnet()
.with_db(db_a)
.build_mainnet_with_inspector(AccessListCollector::default());
let mut evm_a = evm_a;
let (collector, handle) = AccessListCollector::new();
let result_a = evm_a
.inspect_one(tx.clone(), collector)
.expect("inspect runs");
assert!(result_a.is_success(), "fixture must succeed");
let al_collector = handle.take_access_list();
let db_b = fixture_db(contract_addr, Bytecode::new_raw(fixture_bytecode()));
let mut evm_b = RevmContext::mainnet()
.with_db(db_b)
.build_mainnet_with_inspector(AccessListCollector::default());
let result_b = evm_b.transact(tx).expect("transact runs");
assert!(result_b.result.is_success(), "fixture must succeed");
let al_state = emit_access_list_from_state(&result_b.state);
let collector_addrs: Vec<Address> = al_collector.iter().map(|i| i.address).collect();
let state_addrs: Vec<Address> = al_state.iter().map(|i| i.address).collect();
assert_eq!(
collector_addrs,
vec![contract_addr],
"collector surfaces contract only"
);
assert_eq!(
state_addrs,
vec![contract_addr],
"state emitter surfaces contract only"
);
let mut collector_slots = al_collector[0].storage_keys.clone();
let mut state_slots = al_state[0].storage_keys.clone();
collector_slots.sort();
state_slots.sort();
let mut expected = vec![B256::from(U256::from(1u64)), B256::from(U256::from(2u64))];
expected.sort();
assert_eq!(
collector_slots, expected,
"collector slots {collector_slots:?}"
);
assert_eq!(state_slots, expected, "state emitter slots {state_slots:?}");
assert_eq!(
collector_slots, state_slots,
"ADR-019 D3 parity: collector AL must equal state-journal AL"
);
}
}