The Ethereum Virtual Machine (EVM) is the runtime environment that executes smart contract code on Ethereum. Every node in the network runs an identical copy of the EVM. When a transaction targets a smart contract, the node loads the contract's bytecode from the blockchain state and feeds it to the EVM, which processes it instruction by instruction. Because every node executes the same bytecode with the same inputs and the same rules, they all arrive at the same result. This deterministic execution is what allows thousands of independent machines to agree on the state of the blockchain without trusting each other.
The EVM is a stack-based virtual machine. Unlike register-based architectures (like x86 or ARM), it does not have named registers. Instead, all computations happen by pushing values onto a stack and popping them off. It also has access to two other data regions: memory (a temporary byte array that exists only for the duration of the call) and storage (a persistent key-value store that lives on-chain between transactions).
Developers write smart contracts in high-level languages like Solidity or Vyper. The compiler (solc for Solidity) translates the source code into bytecode, a sequence of low-level instructions called opcodes. Each opcode is a single byte (hence the name). For example, 0x60 is PUSH1 (push one byte onto the stack), 0x01 is ADD, and 0x55 is SSTORE (write to storage). The EVM has about 140 opcodes covering arithmetic, comparison, memory access, storage access, control flow, logging, and inter-contract communication.
// Solidity
uint256 x = 1 + 2;
// Compiles roughly to these EVM opcodes
PUSH1 0x01 // push 1 onto the stack
PUSH1 0x02 // push 2 onto the stack
ADD // pop both, push 3
PUSH1 0x00 // storage slot 0
SSTORE // store 3 at slot 0
The stack is where all computation happens. It holds up to 1024 items, each 256 bits (32 bytes) wide. Most opcodes pop their inputs from the stack and push their result back onto it. For instance, ADD pops two values, adds them, and pushes the sum. The stack is the only place where the EVM can do arithmetic, comparisons, and bitwise operations.
Memory is a linear byte array that starts empty and can grow as needed. It is volatile: it only exists for the duration of the current call and is wiped when execution returns. Contracts use memory for intermediate computations, ABI encoding/decoding, and passing data between internal function calls. Accessing memory is cheap at first but the cost increases quadratically as you expand it, which prevents contracts from using unbounded amounts.
Storage is a key-value mapping where both keys and values are 256 bits. Unlike memory, storage is persistent: values written to storage survive after the transaction ends and remain on-chain until explicitly overwritten. This is where contract state lives (balances, ownership, mappings, etc.). Storage operations are the most expensive in the EVM: writing a new value to a fresh slot costs 20,000 gas, while a simple addition costs only 3 gas. This pricing reflects the real cost to the network, since storage must be maintained by every node indefinitely.
Every opcode has a gas cost. When you submit a transaction, you specify a gas limit (the maximum amount of computation you are willing to pay for) and a gas price (how much ETH you pay per unit of gas). As the EVM executes each opcode, it subtracts the corresponding gas cost from the remaining budget. If the budget hits zero before execution finishes, the EVM halts with an "out of gas" error: all state changes revert, but the gas is still consumed (the validator still gets paid for the work they did).
This gas mechanism serves two purposes. First, it prevents infinite loops and denial-of-service attacks. Since every instruction costs money, a contract that tries to loop forever will simply run out of gas. Second, it aligns incentives: validators are compensated for the computational resources they expend, and users pay proportionally for the complexity of their transactions.
When the EVM executes a contract, it has access to an execution context containing information about the transaction and the current block. Solidity exposes these through global variables like msg.sender (the address that called the contract), msg.value (the ETH attached to the call), msg.data (the raw calldata), block.number, and block.timestamp. There is also tx.origin, which always refers to the original EOA that initiated the transaction, even if the contract was called by another contract. Using tx.origin for authorization is considered a security anti-pattern because it can be exploited by malicious intermediary contracts.
If something goes wrong during execution, the EVM can revert. A revert undoes all state changes made during the current call (storage writes, ETH transfers, event emissions) and returns an error message to the caller. Common causes of reverts include failed require() checks, division by zero, and out-of-gas errors. The key property is atomicity: either all state changes in a transaction succeed, or none of them do. There is no partial execution. This makes smart contracts much easier to reason about compared to traditional systems where a failure mid-operation can leave data in an inconsistent state.
To learn about how smart contracts are deployed and structured, see Ethereum Smart Contracts. To understand the full journey of a transaction through the network, see Sending Transactions on Ethereum.