Ethereum Transaction Execution Process: A Deep Dive into EVM Workflow

·

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:

  1. Block Validation: Before inserting a block into the blockchain, its validity must be verified.
  2. Mining Process: During the execution of transactions by the worker in the mining process.

Main Functionalities

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

  1. Pre-check: Validates nonce and gas values.
  2. Gas Calculation: Deducts fixed gas costs.
  3. Transaction Execution: Calls EVM to create or execute the transaction.
  4. 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

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

👉 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.