Library
Mastering Ethereum: Building Smart Contracts and DApps · 5 of 15
Mastering Ethereum: Building Smart Contracts and DApps
blockchain HIGH

EVM Internals — Stack Machine, State, Gas, and Bytecode

evm gas opcodes storage bytecode abi turing-completeness

Key Principle

"The EVM is the part of Ethereum that handles smart contract deployment and execution" — a sandboxed, single-threaded, 256-bit stack machine with no system/hardware interface ("completely virtual"). It resembles the JVM/.NET model: a host-agnostic runtime executing its own bytecode, abstracting only computation and storage. The 256-bit word size was chosen "mainly to facilitate native hashing and elliptic curve operations." Its only job is to compute valid state transitions over the world state — a map of 160-bit (20-byte) addresses to accounts, each holding balance (wei), nonce, storage, and code. EOAs have no code and empty storage; only contract accounts have both.

Why This Matters

Quasi-Turing-completeness = gas as the halting bound. The EVM is "quasi" because every execution is capped at a finite number of steps by the gas available. The reason is the halting problem — "we can't tell, just by looking at a program, whether it will take forever or not to execute." A single-threaded world computer with no scheduler, handed an infinite loop, would seize up for everyone. Gas converts the undecidable "will it terminate?" into a pre-paid budget; the ceiling is the block gas limit. Gas is not merely a fee — it is what makes a Turing-complete shared computer safe to run.

Volatile vs. nonvolatile is the gas split. Three data regions: immutable program-code ROM (the bytecode), volatile memory (zeroed each call), and permanent storage (part of Ethereum state). The memory-vs-storage split is the gas split: MLOAD/MSTORE are cheap and discarded; SLOAD/SSTORE are the expensive resource because storage mutates the permanent world state every node must keep forever — "the price internalizes the externality of bloating world state."

The gas-cost hierarchy (durable; specific integers are historical):

  • Pure stack arithmetic/logic ≈ near-free (ADD/SUB/LT/AND ≈ 3; MUL/DIV/MOD ≈ 5).
  • Persistent storage dominates: SSTORE to a fresh slot ~20,000; SLOAD ~200.
  • Reading other accounts is costly: BALANCE ~400, EXTCODESIZE/EXTCODECOPY ~700.
  • Hashing/logging carry overhead: SHA3 ~30; LOG0 375 rising ~375 per indexed topic to LOG4 1875.
  • CREATE 32,000 — the priciest single op. Sending a transaction = 21,000 gas.
  • Call-family gas is "Complicated" (computed at runtime), so static gas estimates are only approximate for any contract making external calls or writing fresh storage.

Gas cost vs. gas price. Cost = units of gas an operation requires (fixed, deterministic). Price = ether paid per unit. Separating them lets the market float the ether↔computation relationship while keeping operation cost invariant to ether's price swings. transaction fee = total gas used × gas price.

Good Examples

Opcode categories (256-bit stack machine; opcodes pop operands, push results; all arithmetic mod 2^256, 0^0 = 1):

  • ArithmeticADD MUL SUB DIV SDIV MOD SMOD ADDMOD MULMOD EXP SIGNEXTEND, plus SHA3. Silent overflow is why defensive contracts need SafeMath-style checks.
  • Stack/memory/storagePOP, MLOAD/MSTORE, MSTORE8, SLOAD/SSTORE, MSIZE, PUSHx (1–32 bytes), DUPx/SWAPx (1–16) — all 3 gas.
  • Control flowSTOP JUMP JUMPI PC JUMPDEST. Jumps may only land on a JUMPDEST, making bytecode statically analyzable.
  • SystemLOGx CREATE CALL CALLCODE RETURN DELEGATECALL STATICCALL REVERT INVALID SELFDESTRUCT. DELEGATECALL runs another account's code while preserving msg.sender and value — basis of upgradeable proxies and the Parity multisig disaster. STATICCALL forbids state changes (safe read-only calls).
  • EnvironmentalORIGIN (originating EOA) vs CALLER (immediate caller) is the distinction behind "never authorize on tx.origin."
  • BlockBLOCKHASH (last 256 blocks only — why it is a poor randomness source), COINBASE TIMESTAMP NUMBER DIFFICULTY GASLIMIT.

Out-of-gas vs. REVERT semantics. A triggering transaction instantiates a fresh EVM (PC=0, storage loaded, memory zeroed, gas budget = what the sender paid) that mutates a throwaway copy of world state, committing to real state only on clean completion. Hitting zero gas throws Out of Gas: execution halts, the sandbox is discarded, no state change persists — except the nonce increment and ether already paid to the block producer. "Out of gas still costs real money — gas pays for resources consumed, not for success." By contrast, REVERT halts, undoes state, and returns remaining gas + a reason string (unlike the old throw/INVALID which burned all gas).

Deployment vs. runtime bytecode. A creation transaction sets to = 0x0, data = deployment bytecode. The EVM runs that code; its output becomes the account's code. The constructor runs once at deploy time then vanishes — never lives on-chain. Runtime bytecode (a strict subset) is all that executes on later calls. Get each with solc --bin (deployment) vs solc --bin-runtime (runtime).

ABI dispatcher and 4-byte selectors. Incoming calldata hits the contract's dispatcher. A function's selector is the first 4 bytes of keccak256 of its canonical signature — keccak256("withdraw(uint256)")0x2e1a7d4d (canonical = uint256, not uint). The dispatcher: (1) checks calldata ≥4 bytes, else jumps to fallback; (2) loads 32 bytes, isolates the top 4 by DIV; (3) EQ-compares against each precomputed selector and JUMPIs to the match. Malformed short calldata hits the fallback rather than a function.

Counterpoints

  • Failure still costs ether: "the sender will be charged a transaction fee, as miners have already performed the computational work up to that point and must be compensated."
  • Storage refunds (negative gas): SELFDESTRUCT refunded 24,000 gas; SSTORE[x] = 0 (nonzero→zero) refunded 15,000 — capped at half the total gas used to stop deletion-profit attacks.
  • Mispriced opcodes are an attack surface: a 2016 gas/real-cost mismatch nearly halted mainnet, fixed by Tangerine Whistle (EIP-150).
  • Block gas limit caps per-block throughput; a tx whose own gas exceeds the limit can never be mined. Miners vote to adjust it by ≤1/1024 per block.

Key Quotes

"This makes the EVM a quasi–Turing-complete machine: it can run any program you feed into it, but only if the program terminates within a particular amount of computation." — Antonopoulos & Wood, Chapter 13

"Although the transaction was unsuccessful, the sender will be charged a transaction fee, as miners have already performed the computational work up to that point and must be compensated for doing so." — Antonopoulos & Wood, Chapter 13

Rules of Thumb

  • Minimize SSTORE/storage writes; computation is cheap, persistent state is the dominant cost.
  • Treat the gas hierarchy as durable but specific integers as historical (repriced repeatedly).
  • Static gas estimates are only approximate for call-heavy or fresh-storage contracts — set limits with margin or pay for nothing on OOG.
  • Prefer REVERT (returns gas + reason) over patterns that burn all gas.
  • Authorize on CALLER (msg.sender), never ORIGIN (tx.origin).
  • Debug deployed contracts via runtime bytecode (solc --bin-runtime); constructor logic is gone.
  • [DATED 2018]: DIFFICULTYblock.prevrandao (Merge); gas repriced over forks (EIP-150/1884/2200/2929); EIP-1559 (2021) replaced sender-set gasPrice with burned base-fee + tip; refunds reduced/removed by EIP-3529.

Related References