Transaction execution acts as a state modifier by taking the current state and a transaction input, then applying the rules of the Ethereum Virtual Machine (EVM) to produce a deterministic new state.
Rules of the Ethereum Virtual Machine (EVM)
Here are the core rules of the EVM:
Execution Model:
- The EVM is a stack-based machine with a stack depth limit of 1024 items
- Each stack item is a 256-bit word
- All arithmetic is performed modulo 2
- Execution proceeds sequentially through bytecode unless a jump instruction is encountered
- Execution halts on explicit STOP, RETURN, REVERT, SELFDESTRUCT, or when an exception occurs
Gas Rules:
- Every operation costs gas, with different opcodes having different costs
- Transactions must specify a gas limit that bounds maximum execution
- If execution runs out of gas, all state changes revert (except gas payment)
- Unused gas is refunded to the caller
- Memory expansion and storage operations have dynamic gas costs
Memory:
- Memory is linear, byte-addressable, and initially zero
- Memory expands on-demand in 32-byte words
- Expansion costs gas quadratically
- Memory is volatile and cleared between external calls
Storage:
- Storage is a persistent key-value store (256-bit keys and values)
- Each contract has its own isolated storage space
- Storage modifications cost significant gas (cold access, warm access, state changes)
- Storage refunds exist for clearing storage slots
Call Stack:
- Maximum call depth is 1024 frames
- Exceeding call depth causes failure
- Each call creates a new execution context
- Calldata, return data, and local memory are context-specific
Account State:
- Each account has: nonce, balance, storage hash, and code hash
- Contract accounts have associated bytecode
- EOAs (externally owned accounts) have no code
Contract Creation:
- Constructor code runs once during deployment
- Constructor returns the runtime bytecode to be stored
- Creation can fail if: out of gas, collision, insufficient balance, or stack depth exceeded
- Maximum deployed code size is 24,576 bytes (EIP-170)
Call Semantics:
- CALL, DELEGATECALL, STATICCALL, and CALLCODE have different context rules
- DELEGATECALL executes in caller’s context (storage, balance, address)
- STATICCALL prohibits state modifications
- Failed calls return 0 on the stack but don’t revert the parent
State Modification Rules:
- Only the currently executing contract can modify its own storage
- Balance transfers require sufficient balance
- SELFDESTRUCT removes contract code and sends balance to beneficiary
- State changes are atomic within a transaction
Exception Handling:
- Exceptions revert all state changes in the current call context
- Parent contexts can catch and handle exceptions from child calls
- Out-of-gas, invalid opcodes, stack overflow/underflow, and invalid jumps all cause exceptions
Determinism:
- Execution must be deterministic given the same input state
- No access to true randomness (though PREVRANDAO provides pseudo-randomness post-Merge)
- Timestamp and block number are accessible but controlled by validators
Code Immutability:
- Once deployed, contract code cannot be changed
- SELFDESTRUCT is the only way to remove code (deprecated in some contexts)
Access Rules:
- Contracts can read any account’s balance, code, and nonce
- Contracts can only write to their own storage
- Calldata and returndata are read-only
Precompiled Contracts:
- Addresses 0x01-0x09 (and beyond) are reserved for precompiled contracts
- These provide cryptographic and utility functions with special gas costs
EIP-specific Rules:
- EIP-1559: Base fee burning mechanism
- EIP-2929/3529: Cold/warm access costs and reduced refunds
- EIP-3541/3540: EOF (EVM Object Format) restrictions
- Various EIPs modify opcodes, gas costs, and behaviors over time
1. Preparation: State Changes Before Execution
Before a transaction’s bytecode is executed, the system performs several preparatory state modifications via the StateDB.Prepare method:
- Access List Initialization: For post-Berlin transactions, the access list is populated with precompiles and the transaction’s specified addresses/slots to handle EIP-2929 gas pricing.
- Transient Storage Reset: Following EIP-1153, transient storage is cleared at the start of the transaction.
- Account Setup: The system ensures the sender and recipient accounts exist. For contract creations, the sender’s nonce is incremented before the initcode runs to prevent address collisions.
- Value Transfer: If the transaction includes Ether, the
Transferfunction moves the balance from the caller to the recipient before the contract code executes.
2. Handling Specific Transaction Types
The system uses an envelope-based approach (EIP-2718) to distinguish and execute different transaction types:
- Legacy (0x00) & Dynamic Fee (0x02): These are standard transactions. Type 0x02 supports EIP-1559 features like
BaseFeeandGasTipCap. - Access List (0x01): Specifically includes a list of addresses and storage keys to “warm up” the state, reducing the cost of subsequent reads during execution.
- Blob Transactions (0x03): Used for EIP-4844 data blobs. Execution involves checking the blob gas limit (max blobs per block) and updating the header’s
BlobGasUsedbased on the transaction’s sidecar data. - SetCode (0x04): Implements EIP-7702, allowing accounts to have “delegation designators.” During execution, the EVM resolves the code by following the delegation to the target account.
- Deposit Transactions (0x7E): Specific to the OP Stack. These can be System Transactions, which are executed in an unmetered environment and do not count against the block gas limit.
3. Execution Lifecycle Explanation
The exact flow of execution from the miner’s perspective involves the following steps:
- Work Preparation: The miner calls
prepareWorkto create anenvironment. This establishes theBlockContext(number, time, base fee) andTxContext. - Transaction Selection: The miner pulls transactions from the pool, filtered by
MinTipandMaxDATxSize. - Conditional Checks: Before execution, the system checks any
TransactionConditional(e.g., minimum/maximum block number or timestamp). If these fail, the transaction is rejected and evicted from the pool. - The Execution Loop (
EVM.Run):- Opcode Fetching: The interpreter retrieves the instruction from the
JumpTable. - Gas Metering: The system deducts
constantGasand calculatesdynamicGas(e.g., for memory expansion). - State Interaction: Opcodes like
SSTOREorCALLtrigger modifications in thestateObject’s internal caches (dirtyStorage).
- Opcode Fetching: The interpreter retrieves the instruction from the
- Interruption Handling: The worker constantly monitors for interruptions, such as a new head arriving or a recommit signal.
- Finalization: Once execution ends, the system calls
Finaliseto move data from dirty caches to pending storage. - Request Collection (Post-Prague): The system parses logs to collect EIP-6110 deposits, EIP-7002 withdrawals, and EIP-7251 consolidations into the block header.
- Commitment: The final state is committed to the trie, generating a new State Root.
The following details the purpose and mechanics of transaction execution components and their associated Ethereum Improvement Proposals (EIPs).
1. Purpose of Access List Initialization
The initialization of the access list (primarily for EIP-2929 and EIP-2718 transactions) serves to “warm up” specific state items before the EVM begins executing bytecode. By populating this list with precompiles and the addresses/storage keys explicitly mentioned in a transaction’s access list, the system can determine which items should be charged the lower “warm” gas rate rather than the expensive “cold” rate during execution.
2. Precompiled Contracts
Precompiles are specialized cryptographic and mathematical functions implemented directly in the client’s source code rather than as EVM bytecode.
- Efficiency: They handle complex operations that would be too expensive to run in the EVM.
- Examples: Common precompiles include
ecRecover(address recovery from signatures),sha256hash,ripemd160hash, and various elliptic curve addition/multiplication functions (bn256Add,blsG1Mul). - Gas: Each precompile has a specific required gas calculation based on its input size.
3. Purpose of EIP-2929 Gas Pricing
EIP-2929 was introduced to increase the gas cost for state-accessing opcodes to mitigate potential Denial-of-Service (DoS) attacks that exploit “cold” reads from disk.
- Cold vs. Warm Access: It charges a high cost (e.g., 2100 gas) the first time an address or storage slot is accessed in a transaction (“cold”) and a significantly lower cost (e.g., 100 gas) for subsequent accesses (“warm”) within the same transaction.
- Affected Opcodes: This re-pricing affects
SLOAD,SSTORE,EXTCODEHASH,EXTCODESIZE,BALANCE, and theCALLfamily.
4. Transient Storage (EIP-1153)
Transient storage acts as a temporary “scratchpad” for data that exists only for the duration of a single transaction.
- Opcodes: It is accessed via
TLOADandTSTORE. - Non-Persistence: Unlike regular storage, which is committed to the Merkle trie and disk, transient storage is discarded once the transaction completes.
- Use Case: It is highly useful for managing reentrancy guards or passing data between different call frames within one transaction without incurring the high cost of permanent state writes.
5. Envelope-Based Approach (EIP-2718)
The envelope-based approach provides a standardized wrapper for different transaction types, allowing the protocol to introduce new transaction formats without breaking backward compatibility.
- Type Identifier: Each transaction is prepended with a unique type byte (e.g.,
0x01for Access List transactions,0x02for Dynamic Fee transactions). - Decoding Logic: When the system receives a transaction, it checks this leading byte to determine how to decode the subsequent RLP-encoded payload.
6. Transaction Execution Workflow Lifecycle
Below is a simplified code-level lifecycle of how a transaction is handled, from preparation to the main execution loop.
Step 1: Preparation (Prepare) Before execution, the system sets up the environment and initializes caches.
// Pre-execution state preparation
statedb.Prepare(rules, sender, coinbase, recipient, precompiles, txAccessList)
// - Populates access list with precompiles and tx-defined addresses
// - Resets transient storage
Step 2: Initiation (Call) The system moves from the transaction-level into the execution frame.
func (evm *EVM) Call(caller, addr common.Address, input []byte, gas uint64, value *uint256.Int) {
snapshot := evm.StateDB.Snapshot() // Create revert point
if isPrecompile(addr) {
return RunPrecompiledContract(p, input, gas) // Run if it's a precompile
}
// Setup contract environment
contract := NewContract(caller, addr, value, gas, evm.jumpDests)
contract.SetCallCode(hash, evm.StateDB.GetCode(addr))
return evm.Run(contract, input) // Hand over to the interpreter
}
Step 3: The Execution Loop (Run) The EVM interpreter processes individual opcodes.
func (evm *EVM) Run(contract *Contract, input []byte) {
for {
op = contract.GetOp(pc) // Fetch opcode at program counter
operation := jumpTable[op] // Look up opcode logic in JumpTable
// Meter Gas
contract.UseGas(operation.constantGas) // Static cost
if operation.dynamicGas != nil {
cost := operation.dynamicGas(evm, contract, stack, mem, memSize) // EIP-2929/3529 logic
contract.UseGas(cost)
}
// Execute logic (e.g., opAdd, opSstore, etc.)
res, err = operation.execute(&pc, evm, callContext)
if err != nil { break } // Revert or Stop on error
pc++
}
}
Step 4: Finalization Once the loop ends, dirty data is finalized into the pending state caches before the block is committed.