How the Ethereum Virtual Machine Works: Architecture Deep Dive
Understand how the EVM works as a stack machine, including world state, accounts, storage, execution context, the transaction lifecycle, and gas mechanics.
TL;DR: The Ethereum Virtual Machine (EVM) is a stack-based, sandboxed execution environment that runs smart contract bytecode on every Ethereum node. It operates on a 256-bit word size, uses a last-in-first-out stack for computation, provides volatile memory and persistent storage, and uses gas to meter computation. Every state change on Ethereum — from token transfers to DeFi protocol interactions — ultimately executes as a sequence of EVM opcodes. This article explains the architecture from the ground up.
The EVM at a High Level
The Ethereum Virtual Machine is the computation engine at the heart of Ethereum. Its role is straightforward: take bytecode and a set of inputs, execute the bytecode deterministically, and produce a set of state changes. Every full node in the Ethereum network runs the same EVM implementation against the same inputs and must arrive at the same outputs. This deterministic execution is what makes Ethereum a trustless, decentralized computer.
The EVM is deliberately simple compared to modern hardware CPUs. It has no registers, no pipeline, no branch prediction. This simplicity is a feature: it makes the execution model easy to reason about, easy to implement correctly across many languages, and easy to verify.
The EVM is the most widely deployed virtual machine in blockchain, with its instruction set adopted by numerous EVM-compatible chains including Polygon, Arbitrum, Optimism, BNB Chain, and Avalanche.
World State: The Global Database
Ethereum maintains a world state — a mapping from every 20-byte address to an account object. This state is the single source of truth for the entire network.
The world state is stored as a Merkle Patricia Trie, a cryptographic data structure that allows any node to efficiently prove that a particular piece of state exists (or does not exist) without downloading the entire state. The root hash of this trie is included in every block header.
Account Structure
Every account in Ethereum, whether it is a user wallet or a smart contract, has four fields:
| Field | Size | Description |
|---|---|---|
| Nonce | uint64 | Transaction count (EOA) or creation count (contract) |
| Balance | uint256 | Wei balance (1 ETH = 10^18 wei) |
| Storage Root | bytes32 | Root hash of the account’s storage trie |
| Code Hash | bytes32 | Hash of the account’s EVM bytecode |
There are two types of accounts:
Externally Owned Accounts (EOAs) are controlled by private keys. They have no code and no storage. Their nonce tracks how many transactions they have sent. Every transaction on Ethereum originates from an EOA.
Contract Accounts are created by deploying bytecode. They have code (immutable after deployment), storage (mutable), and a nonce that tracks how many contracts they have created. Contract accounts cannot initiate transactions — they only execute when called.
// In Solidity, this contract becomes a Contract Account when deployed
contract SimpleStorage {
uint256 private value; // Stored in the contract's storage trie
function set(uint256 newValue) external {
value = newValue; // SSTORE opcode modifies storage
}
function get() external view returns (uint256) {
return value; // SLOAD opcode reads storage
}
}
The Stack Machine Architecture
The EVM is a stack machine. All computation is performed by pushing values onto and popping values off a stack. There are no general-purpose registers.
Stack Specifications
- Element size: 256 bits (32 bytes) — chosen to match Ethereum’s native word size and hash output size (Keccak-256).
- Maximum depth: 1,024 elements.
- Operations: Most opcodes consume inputs from the top of the stack and push outputs back onto it.
// From the Zig EVM stack implementation
pub const Stack = struct {
items: [1024]u256,
size: usize,
pub fn push(self: *Stack, value: u256) !void {
if (self.size >= 1024) return error.StackOverflow;
self.items[self.size] = value;
self.size += 1;
}
pub fn pop(self: *Stack) !u256 {
if (self.size == 0) return error.StackUnderflow;
self.size -= 1;
return self.items[self.size];
}
};
Why 256-Bit Words?
The 256-bit word size is not arbitrary. Ethereum uses Keccak-256 hashing extensively (for addresses, storage keys, transaction hashes), and elliptic curve cryptography on secp256k1 operates on 256-bit scalars. A 256-bit native word size means these operations can work on single stack elements without splitting values across multiple words.
Memory: The Volatile Workspace
EVM memory is a linear, byte-addressable array that starts empty and can grow as needed during execution. It is volatile: it exists only for the duration of a single message call and is discarded afterward.
Memory Access Patterns
- MLOAD reads 32 bytes starting at a given offset.
- MSTORE writes 32 bytes to a given offset.
- MSTORE8 writes a single byte.
- MSIZE returns the highest offset that has been accessed (rounded up to the nearest 32-byte word).
Memory is cheap for small amounts but becomes expensive quickly. The gas cost for memory is:
cost = 3 * word_count + (word_count^2 / 512)
This quadratic term means that allocating 1 KB of memory costs approximately 99 gas, but 1 MB costs over 3 billion gas — far beyond any block’s gas limit. This design prevents memory-based denial-of-service attacks.
Memory vs. Stack
| Property | Stack | Memory |
|---|---|---|
| Access pattern | LIFO only | Random access |
| Element size | 256 bits fixed | Byte addressable |
| Max size | 1,024 elements | Limited by gas |
| Cost | 3 gas per operation | Quadratic growth |
| Use case | Intermediate values | Data buffers, ABI encoding |
Storage: The Persistent State
Each contract has its own persistent storage — a key-value mapping from 256-bit keys to 256-bit values. Storage is the mechanism by which contracts maintain state between transactions.
Storage is backed by a separate Merkle Patricia Trie per contract. Accessing storage is expensive because the trie must be traversed and, for writes, updated and re-hashed.
| Operation | Gas Cost | Notes |
|---|---|---|
| SLOAD (cold) | 2,100 | First access in transaction |
| SLOAD (warm) | 100 | Subsequent access to same slot |
| SSTORE (zero to non-zero) | 20,000 | Creating a new storage entry |
| SSTORE (non-zero to non-zero) | 5,000 | Modifying existing entry |
| SSTORE (non-zero to zero) | 5,000 + 4,800 refund | Clearing storage |
The “cold” vs. “warm” distinction was introduced in EIP-2929. The first time an address or storage slot is accessed in a transaction, it is “cold” and costs more. Subsequent accesses are “warm” and cheaper, because the data is already cached in the node’s working set.
Execution Context
When the EVM executes code, it operates within an execution context that provides information about the current transaction and block. This context is accessible through dedicated opcodes.
Transaction Context
msg.sender (CALLER) -> Who directly called this code
msg.value (CALLVALUE) -> ETH sent with the call
msg.data (CALLDATALOAD/CALLDATASIZE/CALLDATACOPY) -> Input data
tx.origin (ORIGIN) -> Original EOA that initiated the transaction
tx.gasprice (GASPRICE) -> Gas price of the transaction
Block Context
block.number (NUMBER) -> Current block number
block.timestamp (TIMESTAMP) -> Block timestamp (seconds since epoch)
block.coinbase (COINBASE) -> Address of the block proposer
block.gaslimit (GASLIMIT) -> Block gas limit
block.basefee (BASEFEE) -> EIP-1559 base fee
block.chainid (CHAINID) -> Chain identifier
In the Zig EVM, the execution context is part of the EVM struct:
// Zig EVM execution context
pub const EVM = struct {
stack: Stack,
memory: Memory,
storage: HashMap(u256, u256),
gas_remaining: u64,
gas_limit: u64,
pc: usize, // Program counter
bytecode: []u8,
// Environmental context
caller: Address,
address: Address,
value: u256,
block_number: u64,
timestamp: u64,
// ...
};
The Transaction Lifecycle
Understanding how a transaction flows through the EVM is essential. Here is the complete lifecycle:
Step 1: Transaction Validation
Before execution begins, the transaction is validated:
- The sender’s nonce matches the transaction nonce.
- The sender has enough balance to cover
gas_limit * gas_price + value. - The transaction’s gas limit does not exceed the block gas limit.
Step 2: Intrinsic Gas Deduction
A base cost is deducted before any bytecode executes:
- 21,000 gas for a standard transaction.
- 53,000 gas for contract creation (plus 200 gas per byte of init code since EIP-3860).
- 4 gas per zero byte of calldata, 16 gas per non-zero byte.
Step 3: Execution
The EVM loads the bytecode of the target contract and begins executing from byte 0. For each opcode:
- Read the opcode byte at the current program counter.
- Check that enough gas remains to execute it.
- Deduct the gas cost.
- Execute the opcode (stack manipulation, memory access, etc.).
- Advance the program counter.
Step 4: State Finalization
After execution completes (via STOP, RETURN, REVERT, or running out of gas):
- If successful: state changes are applied, remaining gas is refunded, and any return data is made available.
- If reverted: all state changes are rolled back, but gas consumed up to the revert point is not refunded.
- If out of gas: all state changes are rolled back, and all provided gas is consumed.
# Trace a simple ETH transfer through the lifecycle:
# 1. Validate: sender nonce=5, tx nonce=5 -> OK
# 2. Intrinsic gas: 21,000 (simple transfer, no contract code)
# 3. Execution: No bytecode to execute (EOA recipient)
# 4. Finalize: Deduct value from sender, add to recipient
# Refund remaining gas (gas_limit - 21,000)
The Gas Model
Gas is the EVM’s resource metering system. It serves three purposes:
- Denial-of-service prevention: Without gas, a malicious user could submit an infinite loop and halt the network.
- Resource pricing: Gas costs reflect the real computational cost of operations, ensuring fair payment to validators.
- Execution bounding: Every transaction has a gas limit, guaranteeing termination.
For a comprehensive treatment of gas costs and optimization strategies, see our guide on Understanding EVM Gas.
How Gas Flows
Sender pays: gas_limit * gas_price (deducted from balance upfront)
Execution: consumes gas per opcode
Completion: unused_gas * gas_price (refunded to sender)
Validator: receives gas_used * gas_price as revenue
Message Calls and Contract Interaction
When one contract calls another, the EVM creates a new execution context. The calling contract’s execution is paused, and a new frame is pushed onto the call stack.
Call Types
| Call Type | Opcode | Modifies Caller Storage | Modifies Callee Storage | msg.sender in Callee |
|---|---|---|---|---|
| CALL | 0xF1 | No | Yes | Caller |
| DELEGATECALL | 0xF4 | Yes | No | Original sender |
| STATICCALL | 0xFA | No | Read-only | Caller |
| CALLCODE | 0xF2 | Yes | No | Caller (legacy) |
DELEGATECALL is the foundation of the proxy pattern: the callee’s code executes in the caller’s storage context, enabling upgradeable contracts.
STATICCALL guarantees that no state modifications occur, making it safe for read-only queries.
The maximum call depth is 1,024. If a call chain exceeds this depth, the call fails. This prevents stack-overflow attacks on the network.
Parallel Execution: Beyond Sequential Processing
The standard EVM processes transactions sequentially. However, many transactions in a block are independent — they touch different accounts and storage slots. The Zig EVM project implements parallel transaction execution to exploit this independence.
The approach works in three phases:
- Dependency Analysis: Examine each transaction’s read and write sets (addresses, storage slots, balances). Transactions that share no state are independent.
- Wave Construction: Group independent transactions into “waves” that can execute simultaneously.
- Parallel Execution: Execute each wave using a thread pool, then merge the results.
// Zig EVM parallel execution: O(n) dependency analysis
// Transactions are grouped into waves of independent operations
const wave = analyzer.buildWave(transactions);
// Execute all transactions in the wave concurrently
for (wave.transactions) |tx| {
thread_pool.spawn(executeTx, .{tx});
}
thread_pool.waitAll();
The optimized parallel executor achieves 5-6x throughput improvement for realistic transaction batches. You can experiment with this in the Zig EVM Playground or run the benchmarks from the source repository.
Comparison with Other Virtual Machines
| Feature | EVM | JVM | WASM | eBPF |
|---|---|---|---|---|
| Architecture | Stack | Stack | Stack | Register |
| Word size | 256-bit | 32/64-bit | 32/64-bit | 64-bit |
| Deterministic | Yes | No | Yes (in blockchain) | Yes |
| Resource metering | Gas | None | Fuel (in blockchain) | Instruction count |
| Max stack depth | 1,024 | ~10,000 | Implementation-defined | 512 |
| Persistent state | Storage trie | Disk/DB | Host-dependent | Maps |
The EVM’s 256-bit word size and built-in gas metering are its distinguishing features. While other VMs may be faster in raw throughput, the EVM was purpose-built for trustless, metered computation on a decentralized network.
Key Takeaways
- The EVM is a deterministic, stack-based virtual machine that executes smart contract bytecode identically on every Ethereum node.
- It operates on 256-bit words using a 1,024-element stack, volatile byte-addressable memory, and persistent per-contract storage.
- Ethereum maintains a world state as a Merkle Patricia Trie mapping addresses to accounts, each with a nonce, balance, code hash, and storage root.
- The transaction lifecycle consists of validation, intrinsic gas deduction, opcode-by-opcode execution, and state finalization.
- Gas provides denial-of-service protection, resource pricing, and guaranteed termination.
- Memory costs grow quadratically; storage operations are the most expensive common operations.
- Message calls create nested execution contexts with a maximum depth of 1,024.
- Parallel execution of independent transactions (as implemented in the Zig EVM) can improve throughput by 5-6x without sacrificing correctness.
- For a detailed look at individual instructions, see our EVM Opcodes Reference.
- Learn about EVM Gas Costs to understand how the EVM prices each operation.
- Explore Parallel EVM Execution to see how transaction throughput can be improved 5-6x.
- Understand EVM Storage Layout to see how Solidity maps variables to the persistent key-value store.
- Read about EVM Security Vulnerabilities to learn what can go wrong in smart contract execution.
Frequently Asked Questions
- What type of machine is the EVM?
- The EVM is a quasi-Turing-complete, stack-based virtual machine. It processes instructions by pushing values onto and popping values off a 1,024-element stack of 256-bit words. It is 'quasi' Turing-complete because the gas mechanism places a finite bound on computation.
- What is the difference between EOA and contract accounts?
- Externally Owned Accounts (EOAs) are controlled by private keys and have no code. Contract accounts are controlled by their bytecode and are created via contract deployment transactions. Both types have a balance and nonce, but only contract accounts have code and storage.
- How does EVM memory differ from storage?
- Memory is a volatile, byte-addressable linear array that exists only during a single execution context. Storage is a persistent key-value mapping (256-bit keys to 256-bit values) that persists across transactions on the blockchain. Memory is cheap; storage is expensive.
- What happens when a transaction runs out of gas?
- When execution runs out of gas, the EVM halts immediately and all state changes made during that execution are reverted. However, the gas consumed up to the point of failure is not refunded -- the transaction sender still pays for the computation that was performed.
- Can the EVM execute code in parallel?
- The standard EVM executes transactions sequentially. However, research implementations like the Zig EVM project demonstrate parallel transaction execution by analyzing dependencies between transactions and running non-conflicting transactions concurrently, achieving up to 5-6x throughput improvements.
Try Zig EVM
Explore the interactive playground or dive into the source code.