This module implements the Ethereum Virtual Machine (EVM), a stack-based virtual machine that executes Ethereum smart contracts.
At the core there is this the EVMImpl
struct:
pub struct EVMImpl<'a, GSPEC: Spec, DB: Database, const INSPECT: bool> {
data: EVMData<'a, DB>,
inspector: &'a mut dyn Inspector<DB>,
_phantomdata: PhantomData<GSPEC>,
}
Then there is this trait Transact<DBError>
that EVMImpl
implements.
pub trait Transact<DBError> {
/// Do checks that could make transaction fail before call/create
fn preverify_transaction(&mut self) -> Result<(), EVMError<DBError>>;
/// Skip preverification steps and do transaction
fn transact_preverified(&mut self) -> EVMResult<DBError>;
/// Do transaction.
/// InstructionResult InstructionResult, Output for call or Address if we are creating
/// contract, gas spend, gas refunded, State that needs to be applied.
fn transact(&mut self) -> EVMResult<DBError>;
}
The most important function, as you can see, is transact
:
fn transact(&mut self) -> EVMResult<DB::Error> {
self.preverify_transaction()
.and_then(|_| self.transact_preverified())
}
It first calls preverify_transaction
that does some static checks and if it passes it then calls transact_preverified
which does the following operation:
load the coinbase address → EIP-3651: Warm COINBASE. Starts the COINBASE
address warm
// load coinbase
// EIP-3651: Warm COINBASE. Starts the `COINBASE` address warm
if GSPEC::enabled(SHANGHAI) {
self.data
.journaled_state
.initial_account_load(self.data.env.block.coinbase, &[], self.data.db)
.map_err(EVMError::Database)?;
}
self.load_access_list()?;
load the caller account
// load acc
let journal = &mut self.data.journaled_state;
let (caller_account, _) = journal
.load_account(tx_caller, self.data.db)
.map_err(EVMError::Database)?;
reduce gas_limit*gas_price amount of caller account
caller_account.info.balance = caller_account
.info
.balance
.checked_sub(U256::from(tx_gas_limit).saturating_mul(effective_gas_price))
.unwrap_or(U256::ZERO);
touch account to know it is changed
caller_account.mark_touch();
call inner handling of call/create. Here is where the actual transaction takes place
let (exit_reason, ret_gas, output) = match self.data.env.tx.transact_to {
TransactTo::Call(address) => {
...
let (exit, gas, bytes) = self.call(&mut CallInputs {
...
});
(exit, gas, Output::Call(bytes))
}
TransactTo::Create(scheme) => {
let (exit, address, ret_gas, bytes) = self.create(&mut CreateInputs {
...
});
(exit, ret_gas, Output::Create(bytes, address))
}
};
record the gas that is going to be reimbursed if tx is successful
match exit_reason {
return_ok!() => {
gas.erase_cost(ret_gas.remaining());
gas.record_refund(ret_gas.refunded());
}
return_revert!() => {
gas.erase_cost(ret_gas.remaining());
}
_ => {}
}
call finalize
function that does three things in particular:
at the end it returns the results of the transaction and the new state
Ok(ResultAndState { result, state })
Here is a full workflow:
https://www.figma.com/file/T5JZBavzbQ5ORGrnFhW99m/Welcome-to-FigJam?type=whiteboard&node-id=35-2049&t=QiTxEclSR80GqFnA-4