The world computer is a stack machine
Part five took Ethereum apart into accounts — four fields each, a nonce, a balance, a storageRoot, a codeHash — and said the code in a contract account is what decides where the money goes. Part six followed the storageRoot down into the trie and found that a contract's storage is an array of 2^256 slots, and that the balance of an address lives at exactly one of them: keccak256(address . slot). Both posts kept pointing at a thing they refused to open — the codeHash, and the machine that runs the code behind it. This post opens it.
The machine is the EVM, the Ethereum Virtual Machine, and the counterintuitive claim I want to land is that it is far smaller than its reputation. "World computer" sounds like a supercomputer. It is a stack machine — the same kind of pocket-calculator design you'd build in a first compilers course — with 256-bit words instead of 8-bit ones, running one instruction at a time, single-threaded, no clock, no network, no filesystem. Every full node on Earth runs the same one over the same inputs and gets the same output. That sameness is the entire point: it is not powerful, it is agreed upon.
Nothing here is a mockup. The contract the widgets execute is the runtime bytecode of a real ERC-20, compiled with solc 0.8.9 and bundled verbatim — 6,840 bytes of the actual thing. When you step it below and watch it hand back the number 123456789, that number was not typed into the page; it was computed by a mini-EVM reading those bytes and this series' own Keccak-256, one opcode at a time, in your browser. You can starve it of gas and watch the same run die. Let's build up to why it dies.
01 · One machine on every node
Start with the shape of the thing, because it demolishes the mystique. The EVM has four places to keep data and a list of about 150 things it can do to them, and that's the whole computer. The four places:
- a stack of 256-bit words — this is the workbench, LIFO, and almost every opcode reads its arguments off the top and pushes its result back;
- memory — a flat byte array, scratch space, wiped to zero at the start of every transaction and gone at the end;
- storage — a persistent key-value map, 2^256 slots of 32 bytes, the contract's hard drive, the only thing that survives;
- calldata — the read-only input the caller handed in, the arguments to the call.
Why 256 bits? Because a Keccak-256 hash is 256 bits, and an Ethereum address is the low 160 of one, and the whole system is glued together by hashes — making the native word exactly one hash wide means an address, a slot, a hash all fit in a single stack item with nothing hanging off the edge. It's a strange choice for a CPU and an obvious one for a machine whose job is bookkeeping over hashes.
And it runs like JavaScript: single-threaded, one opcode after another, no parallelism inside a transaction. That's not a performance oversight. If two nodes could interleave instructions differently they might disagree on the result, and disagreement is the one thing a consensus machine cannot afford. Determinism is the product. Given the same code, the same calldata, and the same starting storage, the EVM must produce the same bytes on every machine, or the chain forks. So the EVM has no access to anything nondeterministic — no wall clock of its own, no random source (the one opcode that looks random, PREVRANDAO, just reads a number the consensus layer already agreed on), no network, no disk. It is a sandbox on purpose, and the sandbox is what lets thousands of strangers run the same program and trust the same answer.
02 · They refused Turing-completeness; Ethereum priced it
Bitcoin's Script — the little lock we stepped through in part two — has no loops. You cannot write while in it; it is deliberately not Turing-complete. That looked like a limitation until you noticed that every node has to run every script to validate every transaction, and an infinite loop in a lock would be an infinite loop for the entire network. Bitcoin refused Turing-completeness on purpose, trading away expressiveness for a hard guarantee: validation always halts.
Ethereum makes the opposite bet. The EVM has JUMP and JUMPI — it can loop, it can recurse, it is Turing-complete in the textbook sense. Which drags in the textbook problem: you cannot look at an arbitrary program and decide whether it halts. If nodes had to run submitted code to completion, one malicious while (true) {} would freeze the world.
The fix is the cleverest idea in Ethereum, and it is an economic one, not a computer-science one. Every opcode costs gas. ADD costs 3. A memory-touching MSTORE costs 3 plus a little more as memory grows. Reading a cold storage slot costs 2,100. Writing a fresh one costs 20,000. The transaction that starts the computation names a gas limit — the most it's willing to spend — and the EVM decrements a meter as it runs. When the meter hits zero, execution stops. Not "raises an error you can catch" — stops, hard, and every state change the transaction made is rolled back as if it never happened.
So Ethereum is Turing-complete but every actual execution provably halts, because it's bounded not by the logic of the program but by a budget attached to the money paying for it. My old course notes have a lovely phrase for this: the halting problem is solved because the program ends when the gas ends. That's not a solution to the halting problem — Turing is safe — it's a refusal to play the game. The machine will loop as long as you like; you just have to pay by the instruction, and your wallet is finite. They refused Turing-completeness; Ethereum priced it.
That reframes gas from "an annoying fee" to "the thing that makes a shared Turing-complete computer possible at all." It does three jobs at once: it stops infinite loops, it stops denial-of-service by making wasted computation expensive, and it pays the validators who run the machine for you.
03 · Four places to put a byte
The four data regions from section one aren't interchangeable, and the difference between them is measured in gas — which means it's measured in money, which means it shapes how people actually write contracts. Here's the honest hierarchy, cheapest to most expensive:
- Stack is nearly free. Pushing, popping,
DUP,SWAP, arithmetic — 2 or 3 gas each. But the stack is only 1,024 items deep and you can only reach the top 16 withDUP/SWAP, so it's a workbench, not a warehouse. Values smaller than 32 bytes prefer to live here. - Memory is cheap and temporary. A few gas per word, plus a quadratic surcharge as it grows so you can't balloon it for free. It resets to all-zeros at the start of every transaction, which is exactly why Solidity uses it for the throwaway stuff — the arguments you're assembling to hash, the return value you're about to hand back.
- Calldata is read-only and the cheapest place a value can arrive from. It's the transaction's input, sitting there for the contract to read but never to write.
- Storage is the expensive one, and it's expensive because it's the only one that's real. Every write to storage is a write every node must keep forever, folded into the state trie from part six, hashed into the
stateRootin every block header. That's why a coldSLOADis 2,100 gas and anSSTOREinto a fresh slot is 20,000: you are not computing, you are buying permanent space in a database replicated across the entire network. "Memory is RAM, storage is the hard drive" is the analogy from my notes, and it survives — but the honest version is "storage is the hard drive that ten thousand strangers are contractually obligated to keep a copy of."
Watching those numbers drain is the fastest way to feel the difference, so let's watch.
04 · Step it, then run it out of gas on purpose
Below is the EVM, single-stepping. The default program is sixteen opcodes I hand-assembled to answer one question — what is the balance of this address? — because it's the smallest honest program that touches all four regions: it reads the address from calldata, writes it into memory, hashes that memory with KECCAK256 to compute the storage slot (the keccak256(address . slot) rule from part six, live), reads the balance from storage with SLOAD, and RETURNs it. Watch the stack move, the memory grow word by word, and the gas meter drain — it only ever goes down. Then switch to real ERC-20 · balanceOf and step the actual 6,840-byte compiled contract doing the same job through its real dispatcher:
Things worth watching for:
KECCAK256is where an address becomes a slot. Step to it and read the stack: the two 32-byte words in memory — the address, then the number 5 (the slot thebalanceOfmapping was declared at) — go in, and one 256-bit word comes out. That word is the storage key. This is the exact computation part six drew as a diagram; here it's an opcode with a gas cost.- The
SLOADis the cliff. It's one instruction, and on a cold slot it costs 2,100 gas — more than the entire rest of the program combined. That's not arbitrary: it's the price of reaching into the permanent, network-replicated database. Everything before it is arithmetic and scratch memory, nearly free; the one line that touches real state is the one that hurts. - The real contract returns the same number. Switch programs and the 6,840-byte solc output dispatches on the selector, branches to its
balanceOfbody, and hands back123456789— the value seeded into that one storage slot — after 206 opcodes. Same answer, same live computation, just with a hundred and ninety more opcodes of compiler boilerplate wrapped around the identicalKECCAK256/SLOADcore.
Now do the destructive experiment. Drag the gas limit down, or hit starve it, and run again. The meter drains as before, but this time it hits zero — and it hits zero right at the SLOAD, the expensive line, because that's the first opcode the shrunken budget can't afford. The run halts red, REVERTED, and here is the part that matters: the transaction gets undone. In a real contract that had already sent money or updated a balance before running out of gas, all of it rolls back. The chain looks exactly as if the transaction never ran — except the gas you burned trying is gone, spent, non-refundable. My notes put it plainly: if you run out of gas, it reverts the transaction. You pay for the attempt and get nothing.
Three honest caveats, because this is where the toy meets the real machine. First, the gas numbers are approximations of the Berlin/London schedule — cold-versus-warm access costs, roughly EIP-2929/2200 — close enough to teach "gas drains" and to run a contract out of it, but not consensus-exact; a real node's meter would disagree in the third digit. Second, this mini-EVM has no sub-calls — no CALL, DELEGATECALL, or CREATE, no precompiles, no real world-state — so CALLER and ADDRESS are constants I inject, and I only drive the read paths that stay inside a single frame. Third, the opcode set is a teaching subset. The real 2026 EVM has instructions I don't implement: PUSH0 (Shanghai, 2023), transient storage TLOAD/TSTORE and MCOPY (Dencun, 2024), BLOBHASH/BLOBBASEFEE for blobs, CLZ (Fusaka, 2025) — and SELFDESTRUCT, which still exists but was neutered by EIP-6780 and no longer deletes a contract. I name them so you know the map isn't the territory.
One number that survived from my pre-Merge notes untouched: the base cost of any transaction is 21,000 gas, charged before a single line of your code runs — the flat fee for the privilege of touching the chain at all. What changed is where it goes. My notes describe a first-price gas auction where the fee went to the miner; since EIP-1559 the fee has two parts, a base fee that is burned — destroyed, paid to no one — and a small priority tip to the validator. The auction is gone. The 21,000 stayed.
05 · How a call finds its function
The real contract in the last widget did something the hand-assembled toy skipped: it figured out which function you called. A contract is one blob of bytecode with many functions inside it, and the EVM has no notion of "methods" — it just starts executing at byte zero. So Solidity compiles a dispatcher at the top of every contract: a lookup table that reads the first four bytes of the calldata and jumps to the matching function body.
Those four bytes are the selector, and they're the first four bytes of keccak256 of the function's signature. balanceOf(address) hashes to 70a08231… and the selector is 70a08231. That's the whole convention. Pick a function and watch its selector get computed, then matched against the dispatcher's table:
Things worth watching for:
- The selector is a hash truncated to four bytes. The full
keccak256of the signature is 32 bytes; 28 of them are thrown away. Four bytes is 4 billion possibilities — enough that accidental collisions between real function names are rare, few enough that they're findable on purpose, which is a genuine (small) attack surface the ecosystem mostly ignores. - The dispatcher is just
EQandJUMPI. There's no magic method table in the EVM. The compiler emits, in effect, "is the selector70a08231? then jump to offset X" for every public function, in a row. It's the most literalswitchstatement you'll ever see, written in the two branch opcodes the machine actually has. - The signature is text you can hash yourself.
transfer(address,uint256)— no spaces, canonical types — is just a string. Hash it and you geta9059cbb, the selector every wallet on Earth prepends when it sends a token. Nobody registered it anywhere; it falls out of the hash. This is the same "the name is a fingerprint of the bytes" move from the address post, one layer up.
That four-byte selector followed by the arguments is the calldata — the input the very first widget read with CALLDATALOAD. Which is exactly where the next post picks up.
06 · What's next
The mental model, compressed: the EVM is a 256-bit stack machine that runs identically on every node, which is what turns "some code ran" into "everyone agrees this code ran." It is Turing-complete, so it needs a way to guarantee it halts, and that way is gas — a meter that charges for every opcode and drains a budget attached to the transaction's money, so infinite loops just run out of funds. It keeps data in four places priced by how permanent they are: a nearly-free stack, cheap disposable memory, read-only calldata, and storage so expensive precisely because every node keeps it forever. A contract call finds its function by hashing the signature to a four-byte selector and jumping. And when the gas runs out, the whole transaction unwinds and you pay for the wreckage anyway.
Honesty about the toy's edges, in one place: the gas costs are a Berlin/London approximation, not a consensus-exact meter; there are no sub-calls, precompiles, or real world-state, so the injected CALLER/ADDRESS and the single-frame execution are simplifications; and the opcode set is a teaching subset that omits PUSH0, transient storage, MCOPY, the blob opcodes, and CLZ, and treats SELFDESTRUCT as the museum piece EIP-6780 made it. The two contracts, though, are real bytes from a real compiler, and every number the widgets show was computed live from them.
Next, Tokens: we've now watched balanceOf reach into a single storage slot and pull out a number. That's almost the entire idea of a token — an ERC-20 balance is one storage slot, keccak256(holder . slot), and "sending" a token is subtracting from one slot and adding to another. No coins move; a mapping updates. We'll build the whole standard out of the pieces already on this page.
Same promise as always: it's bytes all the way down, no magic, and you can single-step every one of them.