#![expect(clippy::doc_markdown)]
use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use alloy::primitives::{Address, Bytes, B256, U256};
use revm::bytecode::Bytecode;
use revm::context::TxEnv;
use revm::context_interface::result::{
EVMError, ExecutionResult, InvalidTransaction, ResultAndState,
};
use revm::database::CacheDB;
use revm::database_interface::{Database, DatabaseCommit, DatabaseRef};
use revm::primitives::TxKind;
use revm::state::{AccountInfo, EvmState};
use revm::{ExecuteEvm, MainBuilder, MainContext};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReplayableTx {
pub from: Address,
pub to: Option<Address>,
pub value: U256,
pub data: Bytes,
pub gas_limit: u64,
pub max_fee_per_gas: u128,
pub max_priority_fee_per_gas: u128,
pub nonce: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReplayStatus {
Success,
Reverted,
Halted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PredStatus {
Success,
Reverted,
Halted,
AlreadyMined,
}
impl PredStatus {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Success => "success",
Self::Reverted => "reverted",
Self::Halted => "halted",
Self::AlreadyMined => "already_mined",
}
}
}
impl From<ReplayStatus> for PredStatus {
fn from(status: ReplayStatus) -> Self {
match status {
ReplayStatus::Success => Self::Success,
ReplayStatus::Reverted => Self::Reverted,
ReplayStatus::Halted => Self::Halted,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BaseFeeSource {
Projected,
DisabledFallback,
}
#[derive(Debug)]
pub struct ReplayOutcome {
pub status: ReplayStatus,
pub state: EvmState,
pub touched: Vec<(Address, Vec<U256>)>,
pub rpc_reads: u64,
pub wall: Duration,
pub base_fee_source: BaseFeeSource,
}
#[derive(Debug)]
pub struct SequenceOutcome {
pub predecessors: Vec<PredStatus>,
pub predecessor_gas: Vec<u64>,
pub frame: ReplayOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ReplayFrameError {
#[error("gap pending: frame nonce {claimed}, parent expects {expected}")]
GapPending { claimed: u64, expected: u64 },
#[error("already settled: frame nonce {frame} consumed at parent nonce {parent}")]
AlreadySettled { frame: u64, parent: u64 },
#[error("malformed transaction: {raw}")]
MalformedTransaction { raw: std::string::String },
#[error("replay failed: {raw}")]
Other { raw: std::string::String },
#[error("mispriced transaction: {raw}")]
Mispriced { raw: std::string::String },
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SequenceReplayError {
#[error("sequence predecessor at nonce {nonce} failed: {source}")]
Predecessor {
nonce: u64,
#[source]
source: ReplayFrameError,
},
#[error("frame: {0}")]
Frame(ReplayFrameError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScratchBlock {
pub number: u64,
pub timestamp: u64,
pub base_fee_next: u128,
}
#[derive(Debug, Default)]
pub struct FrameRpcCounter {
basic: AtomicU64,
storage: AtomicU64,
}
impl FrameRpcCounter {
#[must_use]
pub fn reads(&self) -> u64 {
self.basic.load(Ordering::Relaxed) + self.storage.load(Ordering::Relaxed)
}
}
#[derive(Debug)]
pub struct ScratchEvm<Db> {
ext: Db,
block: ScratchBlock,
counter: Arc<FrameRpcCounter>,
}
impl<Db: Database> ScratchEvm<Db> {
#[must_use]
pub fn new(ext: Db, block: ScratchBlock) -> Self {
Self::with_counter(ext, block, Arc::new(FrameRpcCounter::default()))
}
#[must_use]
pub fn with_counter(ext: Db, block: ScratchBlock, counter: Arc<FrameRpcCounter>) -> Self {
Self {
ext,
block,
counter,
}
}
#[must_use]
pub fn ext_mut(&mut self) -> &mut Db {
&mut self.ext
}
#[must_use]
pub fn ext(&self) -> &Db {
&self.ext
}
pub fn replay(&mut self, tx: &ReplayableTx) -> Result<ReplayOutcome, ReplayFrameError> {
let started = Instant::now();
let reads_before = self.counter.reads();
let (settled, base_fee_source) =
run_frame_with_fallback(&mut self.ext, &self.block, &tx.tx_env())?;
Ok(outcome(
settled,
self.counter.reads().saturating_sub(reads_before),
started.elapsed(),
base_fee_source,
))
}
pub fn replay_sequence(
&mut self,
prefix: &[ReplayableTx],
frame: &ReplayableTx,
) -> Result<SequenceOutcome, SequenceReplayError> {
let started = Instant::now();
let reads_before = self.counter.reads();
let mut overlay = SequenceDb::new(&mut self.ext);
let mut predecessors = Vec::with_capacity(prefix.len());
let mut predecessor_gas = Vec::with_capacity(prefix.len());
for predecessor in prefix {
match run_frame_with_fallback(&mut overlay, &self.block, &predecessor.tx_env()) {
Ok((settled, _source)) => {
predecessors.push(execution_status(&settled.result).into());
predecessor_gas.push(settled.result.tx_gas_used());
overlay.commit(settled.state);
}
Err(ReplayFrameError::AlreadySettled { .. }) => {
predecessors.push(PredStatus::AlreadyMined);
predecessor_gas.push(0);
}
Err(source) => {
return Err(SequenceReplayError::Predecessor {
nonce: predecessor.nonce,
source,
});
}
}
}
let (settled, base_fee_source) =
run_frame_with_fallback(&mut overlay, &self.block, &frame.tx_env())
.map_err(SequenceReplayError::Frame)?;
Ok(SequenceOutcome {
predecessors,
predecessor_gas,
frame: outcome(
settled,
self.counter.reads().saturating_sub(reads_before),
started.elapsed(),
base_fee_source,
),
})
}
}
#[must_use]
pub fn read_view_word<Ext: DatabaseRef>(ext: &Ext, address: Address, slot: U256) -> Option<U256> {
ext.storage_ref(address, slot).ok()
}
impl ReplayableTx {
fn tx_env(&self) -> TxEnv {
let kind = match self.to {
Some(to) => TxKind::Call(to),
None => TxKind::Create,
};
{
#[expect(clippy::expect_used)] let env = TxEnv::builder()
.caller(self.from)
.kind(kind)
.data(self.data.clone())
.value(self.value)
.gas_limit(self.gas_limit)
.nonce(self.nonce)
.gas_price(self.max_fee_per_gas)
.gas_priority_fee(Some(self.max_priority_fee_per_gas))
.build()
.expect("ReplayableTx fields always build a valid TxEnv");
env
}
}
}
#[derive(Debug)]
pub struct CountingFrameDb<ExtDb> {
inner: ExtDb,
counter: Arc<FrameRpcCounter>,
}
impl<ExtDb> CountingFrameDb<ExtDb> {
#[must_use]
pub fn new(inner: ExtDb, counter: Arc<FrameRpcCounter>) -> Self {
Self { inner, counter }
}
}
impl<ExtDb: DatabaseRef> DatabaseRef for CountingFrameDb<ExtDb> {
type Error = ExtDb::Error;
fn basic_ref(&self, address: Address) -> Result<Option<revm::state::AccountInfo>, Self::Error> {
self.counter.basic.fetch_add(1, Ordering::Relaxed);
self.inner.basic_ref(address)
}
fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
self.counter.storage.fetch_add(1, Ordering::Relaxed);
self.inner.storage_ref(address, index)
}
fn code_by_hash_ref(&self, code_hash: B256) -> Result<revm::bytecode::Bytecode, Self::Error> {
self.inner.code_by_hash_ref(code_hash)
}
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
self.inner.block_hash_ref(number)
}
}
struct SequenceDb<'a, Db> {
overlay: CacheDB<WarmingExt<'a, Db>>,
}
impl<'a, Db: Database> SequenceDb<'a, Db> {
fn new(ext: &'a mut Db) -> Self {
Self {
overlay: CacheDB::new(WarmingExt {
ext: RefCell::new(ext),
}),
}
}
fn commit(&mut self, state: EvmState) {
self.overlay.commit(state);
}
}
impl<Db: Database> Database for SequenceDb<'_, Db> {
type Error = Db::Error;
fn basic(&mut self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
self.overlay.basic(address)
}
fn code_by_hash(&mut self, code_hash: B256) -> Result<Bytecode, Self::Error> {
self.overlay.code_by_hash(code_hash)
}
fn storage(&mut self, address: Address, index: U256) -> Result<U256, Self::Error> {
self.overlay.storage(address, index)
}
fn block_hash(&mut self, number: u64) -> Result<B256, Self::Error> {
self.overlay.block_hash(number)
}
}
struct WarmingExt<'a, Db> {
ext: RefCell<&'a mut Db>,
}
impl<Db: Database> DatabaseRef for WarmingExt<'_, Db> {
type Error = Db::Error;
fn basic_ref(&self, address: Address) -> Result<Option<AccountInfo>, Self::Error> {
self.ext.borrow_mut().basic(address)
}
fn code_by_hash_ref(&self, code_hash: B256) -> Result<Bytecode, Self::Error> {
self.ext.borrow_mut().code_by_hash(code_hash)
}
fn storage_ref(&self, address: Address, index: U256) -> Result<U256, Self::Error> {
self.ext.borrow_mut().storage(address, index)
}
fn block_hash_ref(&self, number: u64) -> Result<B256, Self::Error> {
self.ext.borrow_mut().block_hash(number)
}
}
type FrameEvm<'a, Db> = revm::MainnetEvm<revm::handler::MainnetContext<&'a mut Db>>;
fn run_frame<Db: Database>(
ext: &mut Db,
block: &ScratchBlock,
tx_env: &TxEnv,
disable_base_fee: bool,
) -> Result<ResultAndState, FrameAbort> {
let mut ctx = revm::context::Context::mainnet();
ctx.cfg.disable_balance_check = true;
ctx.cfg.disable_base_fee = disable_base_fee;
let mut evm: FrameEvm<'_, Db> = ctx.with_db(ext).build_mainnet();
evm.ctx.modify_block(|b| {
b.basefee = u64::try_from(block.base_fee_next).unwrap_or(u64::MAX);
b.number = U256::from(block.number);
b.timestamp = U256::from(block.timestamp);
});
evm.transact(tx_env.clone()).map_err(|err| match err {
EVMError::Transaction(InvalidTransaction::GasPriceLessThanBasefee) => {
FrameAbort::BaseFeeRejected
}
EVMError::Transaction(InvalidTransaction::NonceTooHigh { tx, state }) => {
FrameAbort::OtherError(ReplayFrameError::GapPending {
claimed: tx,
expected: state,
})
}
EVMError::Transaction(InvalidTransaction::NonceTooLow { tx, state }) => {
FrameAbort::OtherError(ReplayFrameError::AlreadySettled {
frame: tx,
parent: state,
})
}
EVMError::Transaction(InvalidTransaction::CallGasCostMoreThanGasLimit {
initial_gas,
gas_limit,
}) => FrameAbort::OtherError(ReplayFrameError::MalformedTransaction {
raw: format!("call gas cost ({initial_gas}) exceeds the gas limit ({gas_limit})"),
}),
other => FrameAbort::OtherError(ReplayFrameError::Other {
raw: other.to_string(),
}),
})
}
enum FrameAbort {
BaseFeeRejected,
OtherError(ReplayFrameError),
}
impl std::fmt::Display for FrameAbort {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BaseFeeRejected => {
write!(f, "base-fee projection rejected (fallback also failed)")
}
Self::OtherError(e) => write!(f, "{e}"),
}
}
}
fn run_frame_with_fallback<Db: Database>(
db: &mut Db,
block: &ScratchBlock,
tx_env: &TxEnv,
) -> Result<(ResultAndState, BaseFeeSource), ReplayFrameError> {
match run_frame(db, block, tx_env, false) {
Ok(settled) => Ok((settled, BaseFeeSource::Projected)),
Err(FrameAbort::BaseFeeRejected) => run_frame(db, block, tx_env, true)
.map(|settled| (settled, BaseFeeSource::DisabledFallback))
.map_err(|abort| match abort {
FrameAbort::OtherError(e) => e,
FrameAbort::BaseFeeRejected => ReplayFrameError::Mispriced {
raw: abort.to_string(),
},
}),
Err(FrameAbort::OtherError(e)) => Err(e),
}
}
fn execution_status(result: &ExecutionResult) -> ReplayStatus {
match result {
ExecutionResult::Success { .. } => ReplayStatus::Success,
ExecutionResult::Revert { .. } => ReplayStatus::Reverted,
ExecutionResult::Halt { .. } => ReplayStatus::Halted,
}
}
fn outcome(
settled: ResultAndState,
rpc_reads: u64,
wall: Duration,
base_fee_source: BaseFeeSource,
) -> ReplayOutcome {
let status = execution_status(&settled.result);
let state: EvmState = settled
.state
.into_iter()
.filter(|(_, account)| account.is_touched())
.collect();
let mut touched: Vec<(Address, Vec<U256>)> = state
.iter()
.map(|(address, account)| {
let mut slots: Vec<U256> = account.storage.keys().copied().collect();
slots.sort_unstable();
(*address, slots)
})
.collect();
touched.sort_unstable_by_key(|(address, _)| *address);
ReplayOutcome {
status,
state,
touched,
rpc_reads,
wall,
base_fee_source,
}
}