Understanding Ethereum's Transaction Execution
In our previous analysis of Ethereum's mining module, we explored how miners retrieve transactions from the TxPool and pass them to the worker object. The worker then executes these transactions locally through commitTransaction, generating receipts, updating the world state, and ultimately packaging them into blocks for mining. This section focuses on the detailed process of local transaction execution within commitTransaction.
Key Components of Transaction Execution
func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) {
snap := w.current.state.Snapshot()
receipt, _, err := core.ApplyTransaction(w.config, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, vm.Config{})
if err != nil {
w.current.state.RevertToSnapshot(snap)
return nil, err
}
w.current.txs = append(w.current.txs, tx)
w.current.receipts = append(w.current.receipts, receipt)
return receipt.Logs, nil
}The core.ApplyTransaction function serves as the entry point for transaction execution, which fundamentally relies on the Ethereum Virtual Machine (EVM).
The ApplyTransaction Function
This function is invoked in two primary scenarios:
- Block Validation: Before inserting a block into the blockchain, its validity must be verified.
- Mining Process: During the execution of transactions by the worker in the mining process.
Main Functionalities
- Converts transactions into Messages.
- Initializes an EVM execution environment.
- Executes transactions, modifies the stateDB, and generates receipts.
func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, uint64, error) {
// Convert transaction to Message
msg, err := tx.AsMessage(types.MakeSigner(config, header.Number))
if err != nil {
return nil, 0, err
}
// Initialize execution context
context := NewEVMContext(msg, header, bc, author)
// Create EVM environment
vmenv := vm.NewEVM(context, statedb, config, cfg)
// Execute transaction
_, gas, failed, err := ApplyMessage(vmenv, msg, gp)
if err != nil {
return nil, 0, err
}
// Update world state
var root []byte
if config.IsByzantium(header.Number) {
statedb.Finalise(true)
} else {
root = statedb.IntermediateRoot(config.IsEIP158(header.Number)).Bytes()
}
*usedGas += gas
receipt := types.NewReceipt(root, failed, *usedGas)
receipt.TxHash = tx.Hash()
receipt.GasUsed = gas
if msg.To() == nil {
receipt.ContractAddress = crypto.CreateAddress(vmenv.Context.Origin, tx.Nonce())
}
receipt.Logs = statedb.GetLogs(tx.Hash())
receipt.Bloom = types.CreateBloom(types.Receipts{receipt})
return receipt, gas, err
}StateTransition.TransitionDb
The StateTransition struct represents the transactional working environment:
type StateTransition struct {
gp *GasPool // Gas pool from the block environment
msg Message // Transaction converted to Message
gas uint64 // Remaining gas
gasPrice *big.Int
initialGas uint64 // Initial gas (transaction gasLimit)
value *big.Int // Transaction amount
data []byte // Transaction input (contract code if applicable)
state vm.StateDB // State tree
evm *vm.EVM // EVM object
}TransitionDb Workflow
- Pre-check: Validates nonce and gas values.
- Gas Calculation: Deducts fixed gas costs.
- Transaction Execution: Calls EVM to create or execute the transaction.
- Miner Reward: Compensates the miner for gas used.
func (st *StateTransition) TransitionDb() (ret []byte, usedGas uint64, failed bool, err error) {
if err = st.preCheck(); err != nil {
return
}
msg := st.msg
sender := vm.AccountRef(msg.From())
homestead := st.evm.ChainConfig().IsHomestead(st.evm.BlockNumber)
contractCreation := msg.To() == nil
// Calculate intrinsic gas
gas, err := IntrinsicGas(st.data, contractCreation, homestead)
if err != nil {
return nil, 0, false, err
}
if err = st.useGas(gas); err != nil {
return nil, 0, false, err
}
var (
evm = st.evm
vmerr error
)
if contractCreation {
ret, _, st.gas, vmerr = evm.Create(sender, st.data, st.gas, st.value)
} else {
st.state.SetNonce(msg.From(), st.state.GetNonce(sender.Address())+1)
ret, st.gas, vmerr = evm.Call(sender, st.to(), st.data, st.gas, st.value)
}
if vmerr != nil {
log.Debug("VM returned with error", "err", vmerr)
if vmerr == vm.ErrInsufficientBalance {
return nil, 0, false, vmerr
}
}
st.refundGas()
st.state.AddBalance(st.evm.Coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.gasPrice))
return ret, st.gasUsed(), vmerr != nil, err
}Gas Management
- buyGas: Deducts gas from the pool and the sender's account.
- useGas: Adjusts gas during transaction execution.
- refundGas: Returns unused gas and rewards the sender.
func (st *StateTransition) buyGas() error {
mgval := new(big.Int).Mul(new(big.Int).SetUint64(st.msg.Gas()), st.gasPrice)
if st.state.GetBalance(st.msg.From()).Cmp(mgval) < 0 {
return errInsufficientBalanceForGas
}
if err := st.gp.SubGas(st.msg.Gas()); err != nil {
return err
}
st.gas += st.msg.Gas()
st.initialGas = st.msg.Gas()
st.state.SubBalance(st.msg.From(), mgval)
return nil
}
func (st *StateTransition) useGas(amount uint64) error {
if st.gas < amount {
return vm.ErrOutOfGas
}
st.gas -= amount
return nil
}
func (st *StateTransition) refundGas() {
refund := st.gasUsed() / 2
if refund > st.state.GetRefund() {
refund = st.state.GetRefund()
}
st.gas += refund
remaining := new(big.Int).Mul(new(big.Int).SetUint64(st.gas), st.gasPrice)
st.state.AddBalance(st.msg.From(), remaining)
st.gp.AddGas(st.gas)
}Transaction Execution Paths
- Contract Creation: Calls
evm.Create(). - Regular Transactions/Contract Calls: Calls
evm.Call().
👉 Explore more about Ethereum's EVM
FAQs
What happens if a transaction runs out of gas?
The transaction is reverted, and any changes to the state are discarded. However, the gas used up to that point is not refunded.
How is gas calculated for a transaction?
Gas is calculated based on the complexity of the transaction, including fixed costs (21,000 gas) and additional costs for operations like contract creation or data storage.
What is the role of the EVM in transaction execution?
The EVM interprets and executes smart contract code, ensuring that transactions are processed according to Ethereum's consensus rules.
How are miners rewarded for executing transactions?
Miners receive the gas fees paid by the transaction sender. The reward is calculated as gasUsed * gasPrice.
Can a transaction fail without consuming all its gas?
Yes, if the transaction fails due to an invalid operation (e.g., insufficient balance), it may consume only part of the allocated gas.
👉 Learn more about Ethereum's transaction lifecycle
Next: EVM and Smart Contract Creation
In the next chapter, we'll explore how the EVM creates and executes smart contracts, diving deeper into the relationship between smart contracts and the EVM interpreter.