use alloy_consensus::BlockHeader;
pub use reth_execution_errors::{
BlockExecutionError, BlockValidationError, InternalBlockExecutionError,
};
pub use reth_execution_types::{BlockExecutionOutput, ExecutionOutcome};
use reth_primitives_traits::Block as _;
pub use reth_storage_errors::provider::ProviderError;
use crate::{system_calls::OnStateHook, TxEnvOverrides};
use alloc::{boxed::Box, vec::Vec};
use alloy_eips::eip7685::Requests;
use alloy_primitives::{
map::{DefaultHashBuilder, HashMap},
Address, BlockNumber,
};
use core::fmt::Display;
use reth_consensus::ConsensusError;
use reth_primitives::{BlockWithSenders, NodePrimitives, Receipt};
use reth_prune_types::PruneModes;
use reth_revm::batch::BlockBatchRecord;
use revm::{
db::{states::bundle_state::BundleRetention, BundleState},
State,
};
use revm_primitives::{db::Database, Account, AccountStatus, EvmState};
pub trait Executor<DB> {
type Input<'a>;
type Output;
type Error;
fn init(&mut self, _tx_env_overrides: Box<dyn TxEnvOverrides>) {}
fn execute(self, input: Self::Input<'_>) -> Result<Self::Output, Self::Error>;
fn execute_with_state_closure<F>(
self,
input: Self::Input<'_>,
state: F,
) -> Result<Self::Output, Self::Error>
where
F: FnMut(&State<DB>);
fn execute_with_state_hook<F>(
self,
input: Self::Input<'_>,
state_hook: F,
) -> Result<Self::Output, Self::Error>
where
F: OnStateHook + 'static;
}
pub trait BatchExecutor<DB> {
type Input<'a>;
type Output;
type Error;
fn execute_and_verify_one(&mut self, input: Self::Input<'_>) -> Result<(), Self::Error>;
fn execute_and_verify_many<'a, I>(&mut self, inputs: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Self::Input<'a>>,
{
for input in inputs {
self.execute_and_verify_one(input)?;
}
Ok(())
}
fn execute_and_verify_batch<'a, I>(mut self, batch: I) -> Result<Self::Output, Self::Error>
where
I: IntoIterator<Item = Self::Input<'a>>,
Self: Sized,
{
self.execute_and_verify_many(batch)?;
Ok(self.finalize())
}
fn finalize(self) -> Self::Output;
fn set_tip(&mut self, tip: BlockNumber);
fn set_prune_modes(&mut self, prune_modes: PruneModes);
fn size_hint(&self) -> Option<usize>;
}
pub trait BlockExecutorProvider: Send + Sync + Clone + Unpin + 'static {
type Primitives: NodePrimitives;
type Executor<DB: Database<Error: Into<ProviderError> + Display>>: for<'a> Executor<
DB,
Input<'a> = &'a BlockWithSenders<<Self::Primitives as NodePrimitives>::Block>,
Output = BlockExecutionOutput<<Self::Primitives as NodePrimitives>::Receipt>,
Error = BlockExecutionError,
>;
type BatchExecutor<DB: Database<Error: Into<ProviderError> + Display>>: for<'a> BatchExecutor<
DB,
Input<'a> = &'a BlockWithSenders<<Self::Primitives as NodePrimitives>::Block>,
Output = ExecutionOutcome<<Self::Primitives as NodePrimitives>::Receipt>,
Error = BlockExecutionError,
>;
fn executor<DB>(&self, db: DB) -> Self::Executor<DB>
where
DB: Database<Error: Into<ProviderError> + Display>;
fn batch_executor<DB>(&self, db: DB) -> Self::BatchExecutor<DB>
where
DB: Database<Error: Into<ProviderError> + Display>;
}
#[derive(Debug, Clone)]
pub struct ExecuteOutput<R = Receipt> {
pub receipts: Vec<R>,
pub gas_used: u64,
}
pub trait BlockExecutionStrategy {
type DB: Database;
type Primitives: NodePrimitives;
type Error: From<ProviderError> + core::error::Error;
fn init(&mut self, _tx_env_overrides: Box<dyn TxEnvOverrides>) {}
fn apply_pre_execution_changes(
&mut self,
block: &BlockWithSenders<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<(), Self::Error>;
fn execute_transactions(
&mut self,
block: &BlockWithSenders<<Self::Primitives as NodePrimitives>::Block>,
) -> Result<ExecuteOutput<<Self::Primitives as NodePrimitives>::Receipt>, Self::Error>;
fn apply_post_execution_changes(
&mut self,
block: &BlockWithSenders<<Self::Primitives as NodePrimitives>::Block>,
receipts: &[<Self::Primitives as NodePrimitives>::Receipt],
) -> Result<Requests, Self::Error>;
fn state_ref(&self) -> &State<Self::DB>;
fn state_mut(&mut self) -> &mut State<Self::DB>;
fn with_state_hook(&mut self, _hook: Option<Box<dyn OnStateHook>>) {}
fn finish(&mut self) -> BundleState {
self.state_mut().merge_transitions(BundleRetention::Reverts);
self.state_mut().take_bundle()
}
fn validate_block_post_execution(
&self,
_block: &BlockWithSenders<<Self::Primitives as NodePrimitives>::Block>,
_receipts: &[<Self::Primitives as NodePrimitives>::Receipt],
_requests: &Requests,
) -> Result<(), ConsensusError> {
Ok(())
}
}
pub trait BlockExecutionStrategyFactory: Send + Sync + Clone + Unpin + 'static {
type Primitives: NodePrimitives;
type Strategy<DB: Database<Error: Into<ProviderError> + Display>>: BlockExecutionStrategy<
DB = DB,
Primitives = Self::Primitives,
Error = BlockExecutionError,
>;
fn create_strategy<DB>(&self, db: DB) -> Self::Strategy<DB>
where
DB: Database<Error: Into<ProviderError> + Display>;
}
impl<F> Clone for BasicBlockExecutorProvider<F>
where
F: Clone,
{
fn clone(&self) -> Self {
Self { strategy_factory: self.strategy_factory.clone() }
}
}
#[allow(missing_debug_implementations)]
pub struct BasicBlockExecutorProvider<F> {
strategy_factory: F,
}
impl<F> BasicBlockExecutorProvider<F> {
pub const fn new(strategy_factory: F) -> Self {
Self { strategy_factory }
}
}
impl<F> BlockExecutorProvider for BasicBlockExecutorProvider<F>
where
F: BlockExecutionStrategyFactory,
{
type Primitives = F::Primitives;
type Executor<DB: Database<Error: Into<ProviderError> + Display>> =
BasicBlockExecutor<F::Strategy<DB>>;
type BatchExecutor<DB: Database<Error: Into<ProviderError> + Display>> =
BasicBatchExecutor<F::Strategy<DB>>;
fn executor<DB>(&self, db: DB) -> Self::Executor<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
let strategy = self.strategy_factory.create_strategy(db);
BasicBlockExecutor::new(strategy)
}
fn batch_executor<DB>(&self, db: DB) -> Self::BatchExecutor<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
let strategy = self.strategy_factory.create_strategy(db);
let batch_record = BlockBatchRecord::default();
BasicBatchExecutor::new(strategy, batch_record)
}
}
#[allow(missing_debug_implementations, dead_code)]
pub struct BasicBlockExecutor<S> {
pub(crate) strategy: S,
}
impl<S> BasicBlockExecutor<S> {
pub const fn new(strategy: S) -> Self {
Self { strategy }
}
}
impl<S, DB> Executor<DB> for BasicBlockExecutor<S>
where
S: BlockExecutionStrategy<DB = DB>,
DB: Database<Error: Into<ProviderError> + Display>,
{
type Input<'a> = &'a BlockWithSenders<<S::Primitives as NodePrimitives>::Block>;
type Output = BlockExecutionOutput<<S::Primitives as NodePrimitives>::Receipt>;
type Error = S::Error;
fn init(&mut self, env_overrides: Box<dyn TxEnvOverrides>) {
self.strategy.init(env_overrides);
}
fn execute(mut self, block: Self::Input<'_>) -> Result<Self::Output, Self::Error> {
self.strategy.apply_pre_execution_changes(block)?;
let ExecuteOutput { receipts, gas_used } = self.strategy.execute_transactions(block)?;
let requests = self.strategy.apply_post_execution_changes(block, &receipts)?;
let state = self.strategy.finish();
Ok(BlockExecutionOutput { state, receipts, requests, gas_used })
}
fn execute_with_state_closure<F>(
mut self,
block: Self::Input<'_>,
mut state: F,
) -> Result<Self::Output, Self::Error>
where
F: FnMut(&State<DB>),
{
self.strategy.apply_pre_execution_changes(block)?;
let ExecuteOutput { receipts, gas_used } = self.strategy.execute_transactions(block)?;
let requests = self.strategy.apply_post_execution_changes(block, &receipts)?;
state(self.strategy.state_ref());
let state = self.strategy.finish();
Ok(BlockExecutionOutput { state, receipts, requests, gas_used })
}
fn execute_with_state_hook<H>(
mut self,
block: Self::Input<'_>,
state_hook: H,
) -> Result<Self::Output, Self::Error>
where
H: OnStateHook + 'static,
{
self.strategy.with_state_hook(Some(Box::new(state_hook)));
self.strategy.apply_pre_execution_changes(block)?;
let ExecuteOutput { receipts, gas_used } = self.strategy.execute_transactions(block)?;
let requests = self.strategy.apply_post_execution_changes(block, &receipts)?;
let state = self.strategy.finish();
Ok(BlockExecutionOutput { state, receipts, requests, gas_used })
}
}
#[allow(missing_debug_implementations)]
pub struct BasicBatchExecutor<S>
where
S: BlockExecutionStrategy,
{
pub(crate) strategy: S,
pub(crate) batch_record: BlockBatchRecord<<S::Primitives as NodePrimitives>::Receipt>,
}
impl<S> BasicBatchExecutor<S>
where
S: BlockExecutionStrategy,
{
pub const fn new(
strategy: S,
batch_record: BlockBatchRecord<<S::Primitives as NodePrimitives>::Receipt>,
) -> Self {
Self { strategy, batch_record }
}
}
impl<S, DB> BatchExecutor<DB> for BasicBatchExecutor<S>
where
S: BlockExecutionStrategy<DB = DB, Error = BlockExecutionError>,
DB: Database<Error: Into<ProviderError> + Display>,
{
type Input<'a> = &'a BlockWithSenders<<S::Primitives as NodePrimitives>::Block>;
type Output = ExecutionOutcome<<S::Primitives as NodePrimitives>::Receipt>;
type Error = BlockExecutionError;
fn execute_and_verify_one(&mut self, block: Self::Input<'_>) -> Result<(), Self::Error> {
if self.batch_record.first_block().is_none() {
self.batch_record.set_first_block(block.header().number());
}
self.strategy.apply_pre_execution_changes(block)?;
let ExecuteOutput { receipts, .. } = self.strategy.execute_transactions(block)?;
let requests = self.strategy.apply_post_execution_changes(block, &receipts)?;
self.strategy.validate_block_post_execution(block, &receipts, &requests)?;
let retention = self.batch_record.bundle_retention(block.header().number());
self.strategy.state_mut().merge_transitions(retention);
self.batch_record.save_receipts(receipts)?;
self.batch_record.save_requests(requests);
Ok(())
}
fn finalize(mut self) -> Self::Output {
ExecutionOutcome::new(
self.strategy.state_mut().take_bundle(),
self.batch_record.take_receipts(),
self.batch_record.first_block().unwrap_or_default(),
self.batch_record.take_requests(),
)
}
fn set_tip(&mut self, tip: BlockNumber) {
self.batch_record.set_tip(tip);
}
fn set_prune_modes(&mut self, prune_modes: PruneModes) {
self.batch_record.set_prune_modes(prune_modes);
}
fn size_hint(&self) -> Option<usize> {
Some(self.strategy.state_ref().bundle_state.size_hint())
}
}
pub fn balance_increment_state<DB>(
balance_increments: &HashMap<Address, u128, DefaultHashBuilder>,
state: &mut State<DB>,
) -> Result<EvmState, BlockExecutionError>
where
DB: Database,
{
let mut load_account = |address: &Address| -> Result<(Address, Account), BlockExecutionError> {
let cache_account = state.load_cache_account(*address).map_err(|_| {
BlockExecutionError::msg("could not load account for balance increment")
})?;
let account = cache_account.account.as_ref().ok_or_else(|| {
BlockExecutionError::msg("could not load account for balance increment")
})?;
Ok((
*address,
Account {
info: account.info.clone(),
storage: Default::default(),
status: AccountStatus::Touched,
},
))
};
balance_increments
.iter()
.filter(|(_, &balance)| balance != 0)
.map(|(addr, _)| load_account(addr))
.collect::<Result<EvmState, _>>()
}
#[cfg(test)]
mod tests {
use super::*;
use alloy_primitives::U256;
use core::marker::PhantomData;
use reth_chainspec::{ChainSpec, MAINNET};
use reth_primitives::EthPrimitives;
use revm::db::{CacheDB, EmptyDBTyped};
use revm_primitives::{address, bytes, AccountInfo, TxEnv, KECCAK_EMPTY};
use std::sync::Arc;
#[derive(Clone, Default)]
struct TestExecutorProvider;
impl BlockExecutorProvider for TestExecutorProvider {
type Primitives = EthPrimitives;
type Executor<DB: Database<Error: Into<ProviderError> + Display>> = TestExecutor<DB>;
type BatchExecutor<DB: Database<Error: Into<ProviderError> + Display>> = TestExecutor<DB>;
fn executor<DB>(&self, _db: DB) -> Self::Executor<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
TestExecutor(PhantomData)
}
fn batch_executor<DB>(&self, _db: DB) -> Self::BatchExecutor<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
TestExecutor(PhantomData)
}
}
struct TestExecutor<DB>(PhantomData<DB>);
impl<DB> Executor<DB> for TestExecutor<DB> {
type Input<'a> = &'a BlockWithSenders;
type Output = BlockExecutionOutput<Receipt>;
type Error = BlockExecutionError;
fn execute(self, _input: Self::Input<'_>) -> Result<Self::Output, Self::Error> {
Err(BlockExecutionError::msg("execution unavailable for tests"))
}
fn execute_with_state_closure<F>(
self,
_: Self::Input<'_>,
_: F,
) -> Result<Self::Output, Self::Error>
where
F: FnMut(&State<DB>),
{
Err(BlockExecutionError::msg("execution unavailable for tests"))
}
fn execute_with_state_hook<F>(
self,
_: Self::Input<'_>,
_: F,
) -> Result<Self::Output, Self::Error>
where
F: OnStateHook,
{
Err(BlockExecutionError::msg("execution unavailable for tests"))
}
}
impl<DB> BatchExecutor<DB> for TestExecutor<DB> {
type Input<'a> = &'a BlockWithSenders;
type Output = ExecutionOutcome;
type Error = BlockExecutionError;
fn execute_and_verify_one(&mut self, _input: Self::Input<'_>) -> Result<(), Self::Error> {
Ok(())
}
fn finalize(self) -> Self::Output {
todo!()
}
fn set_tip(&mut self, _tip: BlockNumber) {
todo!()
}
fn set_prune_modes(&mut self, _prune_modes: PruneModes) {
todo!()
}
fn size_hint(&self) -> Option<usize> {
None
}
}
struct TestExecutorStrategy<DB, EvmConfig> {
_chain_spec: Arc<ChainSpec>,
_evm_config: EvmConfig,
state: State<DB>,
execute_transactions_result: ExecuteOutput<Receipt>,
apply_post_execution_changes_result: Requests,
finish_result: BundleState,
}
#[derive(Clone)]
struct TestExecutorStrategyFactory {
execute_transactions_result: ExecuteOutput<Receipt>,
apply_post_execution_changes_result: Requests,
finish_result: BundleState,
}
impl BlockExecutionStrategyFactory for TestExecutorStrategyFactory {
type Primitives = EthPrimitives;
type Strategy<DB: Database<Error: Into<ProviderError> + Display>> =
TestExecutorStrategy<DB, TestEvmConfig>;
fn create_strategy<DB>(&self, db: DB) -> Self::Strategy<DB>
where
DB: Database<Error: Into<ProviderError> + Display>,
{
let state = State::builder()
.with_database(db)
.with_bundle_update()
.without_state_clear()
.build();
TestExecutorStrategy {
_chain_spec: MAINNET.clone(),
_evm_config: TestEvmConfig {},
execute_transactions_result: self.execute_transactions_result.clone(),
apply_post_execution_changes_result: self
.apply_post_execution_changes_result
.clone(),
finish_result: self.finish_result.clone(),
state,
}
}
}
impl<DB> BlockExecutionStrategy for TestExecutorStrategy<DB, TestEvmConfig>
where
DB: Database,
{
type DB = DB;
type Primitives = EthPrimitives;
type Error = BlockExecutionError;
fn apply_pre_execution_changes(
&mut self,
_block: &BlockWithSenders,
) -> Result<(), Self::Error> {
Ok(())
}
fn execute_transactions(
&mut self,
_block: &BlockWithSenders,
) -> Result<ExecuteOutput<Receipt>, Self::Error> {
Ok(self.execute_transactions_result.clone())
}
fn apply_post_execution_changes(
&mut self,
_block: &BlockWithSenders,
_receipts: &[Receipt],
) -> Result<Requests, Self::Error> {
Ok(self.apply_post_execution_changes_result.clone())
}
fn state_ref(&self) -> &State<DB> {
&self.state
}
fn state_mut(&mut self) -> &mut State<DB> {
&mut self.state
}
fn with_state_hook(&mut self, _hook: Option<Box<dyn OnStateHook>>) {}
fn finish(&mut self) -> BundleState {
self.finish_result.clone()
}
fn validate_block_post_execution(
&self,
_block: &BlockWithSenders,
_receipts: &[Receipt],
_requests: &Requests,
) -> Result<(), ConsensusError> {
Ok(())
}
}
#[derive(Clone)]
struct TestEvmConfig {}
#[test]
fn test_provider() {
let provider = TestExecutorProvider;
let db = CacheDB::<EmptyDBTyped<ProviderError>>::default();
let executor = provider.executor(db);
let _ = executor.execute(&Default::default());
}
#[test]
fn test_strategy() {
let expected_gas_used = 10;
let expected_receipts = vec![Receipt::default()];
let expected_execute_transactions_result = ExecuteOutput::<Receipt> {
receipts: expected_receipts.clone(),
gas_used: expected_gas_used,
};
let expected_apply_post_execution_changes_result = Requests::new(vec![bytes!("deadbeef")]);
let expected_finish_result = BundleState::default();
let strategy_factory = TestExecutorStrategyFactory {
execute_transactions_result: expected_execute_transactions_result,
apply_post_execution_changes_result: expected_apply_post_execution_changes_result
.clone(),
finish_result: expected_finish_result.clone(),
};
let provider = BasicBlockExecutorProvider::new(strategy_factory);
let db = CacheDB::<EmptyDBTyped<ProviderError>>::default();
let executor = provider.executor(db);
let result = executor.execute(&Default::default());
assert!(result.is_ok());
let block_execution_output = result.unwrap();
assert_eq!(block_execution_output.gas_used, expected_gas_used);
assert_eq!(block_execution_output.receipts, expected_receipts);
assert_eq!(block_execution_output.requests, expected_apply_post_execution_changes_result);
assert_eq!(block_execution_output.state, expected_finish_result);
}
#[test]
fn test_tx_env_overrider() {
let strategy_factory = TestExecutorStrategyFactory {
execute_transactions_result: ExecuteOutput {
receipts: vec![Receipt::default()],
gas_used: 10,
},
apply_post_execution_changes_result: Requests::new(vec![bytes!("deadbeef")]),
finish_result: BundleState::default(),
};
let provider = BasicBlockExecutorProvider::new(strategy_factory);
let db = CacheDB::<EmptyDBTyped<ProviderError>>::default();
let mut executor = provider.executor(db);
executor.init(Box::new(|tx_env: &mut TxEnv| {
tx_env.nonce.take();
}));
let result = executor.execute(&Default::default());
assert!(result.is_ok());
}
fn setup_state_with_account(
addr: Address,
balance: u128,
nonce: u64,
) -> State<CacheDB<EmptyDBTyped<BlockExecutionError>>> {
let db = CacheDB::<EmptyDBTyped<BlockExecutionError>>::default();
let mut state = State::builder().with_database(db).with_bundle_update().build();
let account_info = AccountInfo {
balance: U256::from(balance),
nonce,
code_hash: KECCAK_EMPTY,
code: None,
};
state.insert_account(addr, account_info);
state
}
#[test]
fn test_balance_increment_state_zero() {
let addr = address!("1000000000000000000000000000000000000000");
let mut state = setup_state_with_account(addr, 100, 1);
let mut increments = HashMap::<Address, u128, DefaultHashBuilder>::default();
increments.insert(addr, 0);
let result = balance_increment_state(&increments, &mut state).unwrap();
assert!(result.is_empty(), "Zero increments should be ignored");
}
#[test]
fn test_balance_increment_state_empty_increments_map() {
let mut state = State::builder()
.with_database(CacheDB::<EmptyDBTyped<BlockExecutionError>>::default())
.with_bundle_update()
.build();
let increments = HashMap::<Address, u128, DefaultHashBuilder>::default();
let result = balance_increment_state(&increments, &mut state).unwrap();
assert!(result.is_empty(), "Empty increments map should return empty state");
}
#[test]
fn test_balance_increment_state_multiple_valid_increments() {
let addr1 = address!("1000000000000000000000000000000000000000");
let addr2 = address!("2000000000000000000000000000000000000000");
let mut state = setup_state_with_account(addr1, 100, 1);
let account2 =
AccountInfo { balance: U256::from(200), nonce: 1, code_hash: KECCAK_EMPTY, code: None };
state.insert_account(addr2, account2);
let mut increments = HashMap::<Address, u128, DefaultHashBuilder>::default();
increments.insert(addr1, 50);
increments.insert(addr2, 100);
let result = balance_increment_state(&increments, &mut state).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result.get(&addr1).unwrap().info.balance, U256::from(100));
assert_eq!(result.get(&addr2).unwrap().info.balance, U256::from(200));
}
#[test]
fn test_balance_increment_state_mixed_zero_and_nonzero_increments() {
let addr1 = address!("1000000000000000000000000000000000000000");
let addr2 = address!("2000000000000000000000000000000000000000");
let mut state = setup_state_with_account(addr1, 100, 1);
let account2 =
AccountInfo { balance: U256::from(200), nonce: 1, code_hash: KECCAK_EMPTY, code: None };
state.insert_account(addr2, account2);
let mut increments = HashMap::<Address, u128, DefaultHashBuilder>::default();
increments.insert(addr1, 0);
increments.insert(addr2, 100);
let result = balance_increment_state(&increments, &mut state).unwrap();
assert_eq!(result.len(), 1, "Only non-zero increments should be included");
assert!(!result.contains_key(&addr1), "Zero increment account should not be included");
assert_eq!(result.get(&addr2).unwrap().info.balance, U256::from(200));
}
}