feat: Hook on Execution (#1567)

This commit is contained in:
Georgios Konstantopoulos
2023-02-27 08:21:34 -07:00
committed by GitHub
parent 7275f8d922
commit 56394eec2c
5 changed files with 312 additions and 33 deletions

1
Cargo.lock generated
View File

@@ -4598,6 +4598,7 @@ dependencies = [
"reth-primitives",
"reth-provider",
"reth-revm",
"reth-revm-inspectors",
"reth-rlp",
"revm",
"rlp",

View File

@@ -11,6 +11,7 @@ readme = "README.md"
reth-primitives = { path = "../primitives" }
reth-interfaces = { path = "../interfaces" }
reth-revm = { path = "../revm" }
reth-revm-inspectors = { path = "../revm/revm-inspectors" }
reth-rlp = { path = "../rlp" }
reth-db = { path = "../storage/db" }
reth-provider = { path = "../storage/provider" }

View File

@@ -1,6 +1,7 @@
use crate::execution_result::{
AccountChangeSet, AccountInfoChangeSet, ExecutionResult, TransactionChangeSet,
};
use hashbrown::hash_map::Entry;
use reth_interfaces::executor::{BlockExecutor, Error};
use reth_primitives::{
@@ -14,6 +15,7 @@ use reth_revm::{
env::{fill_cfg_and_block_env, fill_tx_env},
into_reth_log, to_reth_acc,
};
use reth_revm_inspectors::stack::{InspectorStack, InspectorStackConfig};
use revm::{
db::AccountState,
primitives::{Account as RevmAccount, AccountInfo, Bytecode, ResultAndState},
@@ -28,19 +30,24 @@ where
{
chain_spec: &'a ChainSpec,
evm: EVM<&'a mut SubState<DB>>,
/// Enable revm inspector printer.
/// In execution this will print opcode level traces directly to console.
pub use_printer_tracer: bool,
stack: InspectorStack,
}
impl<'a, DB> Executor<'a, DB>
where
DB: StateProvider,
{
fn new(chain_spec: &'a ChainSpec, db: &'a mut SubState<DB>) -> Self {
/// Creates a new executor from the given chain spec and database.
pub fn new(chain_spec: &'a ChainSpec, db: &'a mut SubState<DB>) -> Self {
let mut evm = EVM::new();
evm.database(db);
Executor { chain_spec, evm, use_printer_tracer: false }
Executor { chain_spec, evm, stack: InspectorStack::new(InspectorStackConfig::default()) }
}
/// Configures the executor with the given inspectors.
pub fn with_stack(mut self, stack: InspectorStack) -> Self {
self.stack = stack;
self
}
fn db(&mut self) -> &mut SubState<DB> {
@@ -327,18 +334,44 @@ where
}
}
}
}
impl<'a, DB> BlockExecutor<ExecutionResult> for Executor<'a, DB>
where
DB: StateProvider,
{
fn execute(
/// Runs a single transaction in the configured environment and proceeds
/// to return the result and state diff (without applying it).
///
/// Assumes the rest of the block environment has been filled via `init_block_env`.
pub fn transact(
&mut self,
transaction: &TransactionSigned,
sender: Address,
) -> Result<ResultAndState, Error> {
// Fill revm structure.
fill_tx_env(&mut self.evm.env.tx, transaction, sender);
let out = if self.stack.should_inspect(&self.evm.env, transaction.hash()) {
// execution with inspector.
let output = self.evm.inspect(&mut self.stack);
tracing::trace!(
target: "evm",
hash = ?transaction.hash(), ?output, ?transaction, env = ?self.evm.env,
"Executed transaction"
);
output
} else {
// main execution.
self.evm.transact()
};
out.map_err(|e| Error::EVM(format!("{e:?}")))
}
/// Runs the provided transactions and commits their state. Will proceed
/// to return the total gas used by this batch of transaction as well as the
/// changesets generated by each tx.
pub fn execute_transactions(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<ExecutionResult, Error> {
) -> Result<(Vec<TransactionChangeSet>, u64), Error> {
let senders = self.recover_senders(&block.body, senders)?;
self.init_env(&block.header, total_difficulty);
@@ -357,27 +390,8 @@ where
block_available_gas,
})
}
// Fill revm structure.
fill_tx_env(&mut self.evm.env.tx, transaction, sender);
// Execute transaction.
let out = if self.use_printer_tracer {
// execution with inspector.
let output = self.evm.inspect(revm::inspectors::CustomPrintTracer::default());
tracing::trace!(
target: "evm",
hash = ?transaction.hash(), ?output, ?transaction, env = ?self.evm.env,
"Executed transaction"
);
output
} else {
// main execution.
self.evm.transact()
};
// cast the error and extract returnables.
let ResultAndState { result, state } = out.map_err(|e| Error::EVM(format!("{e:?}")))?;
let ResultAndState { result, state } = self.transact(transaction, sender)?;
// commit changes
let (changeset, new_bytecodes) = self.commit_changes(state);
@@ -404,6 +418,23 @@ where
});
}
Ok((tx_changesets, cumulative_gas_used))
}
}
impl<'a, DB> BlockExecutor<ExecutionResult> for Executor<'a, DB>
where
DB: StateProvider,
{
fn execute(
&mut self,
block: &Block,
total_difficulty: U256,
senders: Option<Vec<Address>>,
) -> Result<ExecutionResult, Error> {
let (tx_changesets, cumulative_gas_used) =
self.execute_transactions(block, total_difficulty, senders)?;
// Check if gas used matches the value set in header.
if block.gas_used != cumulative_gas_used {
return Err(Error::BlockGasUsed { got: cumulative_gas_used, expected: block.gas_used })
@@ -485,7 +516,8 @@ pub fn execute<DB: StateProvider>(
chain_spec: &ChainSpec,
db: &mut SubState<DB>,
) -> Result<ExecutionResult, Error> {
let mut executor = Executor::new(chain_spec, db);
let mut executor = Executor::new(chain_spec, db)
.with_stack(InspectorStack::new(InspectorStackConfig::default()));
executor.execute(block, total_difficulty, senders)
}

View File

@@ -9,3 +9,8 @@
/// An inspector implementation for an EIP2930 Accesslist
pub mod access_list;
/// An inspector stack abstracting the implementation details of
/// each inspector and allowing to hook on block/transaciton execution,
/// used in the main RETH executor.
pub mod stack;

View File

@@ -0,0 +1,240 @@
use reth_primitives::{bytes::Bytes, Address, TxHash, H256};
use revm::{
inspectors::CustomPrintTracer,
interpreter::{CallInputs, CreateInputs, Gas, InstructionResult, Interpreter},
primitives::Env,
Database, EVMData, Inspector,
};
/// One can hook on inspector execution in 3 ways:
/// - Block: Hook on block execution
/// - BlockWithIndex: Hook on block execution transaction index
/// - Transaction: Hook on a specific transaction hash
#[derive(Default)]
pub enum Hook {
#[default]
/// No hook.
None,
/// Hook on a specific block.
Block(u64),
/// Hook on a specific transaction hash.
Transaction(TxHash),
/// Hooks on every transaction in a block.
All,
}
#[derive(Default)]
/// An inspector that calls multiple inspectors in sequence.
///
/// If a call to an inspector returns a value other than [InstructionResult::Continue] (or
/// equivalent) the remaining inspectors are not called.
pub struct InspectorStack {
/// An inspector that prints the opcode traces to the console.
pub custom_print_tracer: Option<CustomPrintTracer>,
/// The provided hook
pub hook: Hook,
}
impl InspectorStack {
/// Create a new inspector stack.
pub fn new(config: InspectorStackConfig) -> Self {
let mut stack = InspectorStack { hook: config.hook, ..Default::default() };
if config.use_printer_tracer {
stack.custom_print_tracer = Some(CustomPrintTracer::default());
}
stack
}
/// Check if the inspector should be used.
pub fn should_inspect(&self, env: &Env, tx_hash: TxHash) -> bool {
match self.hook {
Hook::None => false,
Hook::Block(block) => env.block.number.to::<u64>() == block,
Hook::Transaction(hash) => hash == tx_hash,
Hook::All => true,
}
}
}
#[derive(Default)]
/// Configuration for the inspectors.
pub struct InspectorStackConfig {
/// Enable revm inspector printer.
/// In execution this will print opcode level traces directly to console.
pub use_printer_tracer: bool,
/// Hook on a specific block or transaction.
pub hook: Hook,
}
/// Helper macro to call the same method on multiple inspectors without resorting to dynamic
/// dispatch
#[macro_export]
macro_rules! call_inspectors {
($id:ident, [ $($inspector:expr),+ ], $call:block) => {
$({
if let Some($id) = $inspector {
$call;
}
})+
}
}
impl<DB> Inspector<DB> for InspectorStack
where
DB: Database,
{
fn initialize_interp(
&mut self,
interpreter: &mut Interpreter,
data: &mut EVMData<'_, DB>,
is_static: bool,
) -> InstructionResult {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let status = inspector.initialize_interp(interpreter, data, is_static);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return status
}
});
InstructionResult::Continue
}
fn step(
&mut self,
interpreter: &mut Interpreter,
data: &mut EVMData<'_, DB>,
is_static: bool,
) -> InstructionResult {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let status = inspector.step(interpreter, data, is_static);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return status
}
});
InstructionResult::Continue
}
fn log(
&mut self,
evm_data: &mut EVMData<'_, DB>,
address: &Address,
topics: &[H256],
data: &Bytes,
) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
inspector.log(evm_data, address, topics, data);
});
}
fn step_end(
&mut self,
interpreter: &mut Interpreter,
data: &mut EVMData<'_, DB>,
is_static: bool,
eval: InstructionResult,
) -> InstructionResult {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let status = inspector.step_end(interpreter, data, is_static, eval);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return status
}
});
InstructionResult::Continue
}
fn call(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CallInputs,
is_static: bool,
) -> (InstructionResult, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (status, gas, retdata) = inspector.call(data, inputs, is_static);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return (status, gas, retdata)
}
});
(InstructionResult::Continue, Gas::new(inputs.gas_limit), Bytes::new())
}
fn call_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CallInputs,
remaining_gas: Gas,
ret: InstructionResult,
out: Bytes,
is_static: bool,
) -> (InstructionResult, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (new_ret, new_gas, new_out) =
inspector.call_end(data, inputs, remaining_gas, ret, out.clone(), is_static);
// If the inspector returns a different ret or a revert with a non-empty message,
// we assume it wants to tell us something
if new_ret != ret || (new_ret == InstructionResult::Revert && new_out != out) {
return (new_ret, new_gas, new_out)
}
});
(ret, remaining_gas, out)
}
fn create(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &mut CreateInputs,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (status, addr, gas, retdata) = inspector.create(data, inputs);
// Allow inspectors to exit early
if status != InstructionResult::Continue {
return (status, addr, gas, retdata)
}
});
(InstructionResult::Continue, None, Gas::new(inputs.gas_limit), Bytes::new())
}
fn create_end(
&mut self,
data: &mut EVMData<'_, DB>,
inputs: &CreateInputs,
ret: InstructionResult,
address: Option<Address>,
remaining_gas: Gas,
out: Bytes,
) -> (InstructionResult, Option<Address>, Gas, Bytes) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
let (new_ret, new_address, new_gas, new_retdata) =
inspector.create_end(data, inputs, ret, address, remaining_gas, out.clone());
if new_ret != ret {
return (new_ret, new_address, new_gas, new_retdata)
}
});
(ret, address, remaining_gas, out)
}
fn selfdestruct(&mut self) {
call_inspectors!(inspector, [&mut self.custom_print_tracer], {
Inspector::<DB>::selfdestruct(inspector);
});
}
}