Ethereum doesn't have coins, it has a database

COMMIT536605dHEAD → main
PUBLISHEDJan 25, 20224y ago
UPDATEDJul 25, 20265d ago
READING13 min1,917 words
#ethereum#storage·by santiago toscanini
Ethereum·Part 1 of 6

The Bitcoin half of this series ended on a promise: Ethereum looked at Bitcoin's design — the chain of headers, the coins, the Merkle proofs — kept the chain, and threw away almost everything else. This post is the throwing-away. It's also where my old university notes stop aging gracefully and start being wrong, because they were written before September 2022, and in September 2022 Ethereum stopped being the thing my notes describe. So this is two corrections at once: Bitcoin's model into Ethereum's, and 2021's Ethereum into 2026's.

Start with the single sharpest difference, the one part two set up without naming. In Bitcoin, there are no accounts. Your wallet's "balance" is a fiction your software computes by scanning for coins — unspent transaction outputs — that happen to be locked to keys you hold. An address holds no state; it's just a pattern that some outputs match. Ethereum made the opposite choice. An address holds state directly: a balance, a counter, and — if it's a contract — code and storage. There is one giant table, keyed by address, and it is the whole point. Bitcoin is trying to be money. Ethereum is trying to be a database that money runs on top of.

Nothing here is a mockup. I fetched a real Ethereum block — number 25,599,433, the chain tip — from a public node, along with three real accounts read at that exact block with eth_getProof. Every hash the figures can recompute, they do: the empty-code hash, the empty-storage root, a real contract's code hash re-derived from its actual bytecode, all computed in your browser by the same little library that carried its own SHA-256 through the Bitcoin posts. This one carries its own Keccak.

01 · A row, not a pile of coins

Here is the reframe, stated as plainly as I can. A Bitcoin balance is a pile: a set of discrete coins, each one indivisible, each spent exactly once and then gone, replaced by new coins paid to new owners. To pay someone you destroy some coins and mint others; there is no number anywhere that says "this address has 3.5 BTC," only a pile you add up. An Ethereum balance is a cell in a table. It is a number, sitting in a row, and paying someone decrements one row and increments another. Nothing is destroyed. Nothing is minted. A field changes.

My notes called these rows something nobody else does. The course taught two account types: EOAs, and — for contracts — "IOAs, Internally Owned Accounts." I have never once seen anyone else use that term; the rest of the planet says "contract account." I'm keeping the slip because the distinction it was reaching for is real and it's the second-most-important idea in this post. An externally-owned account (EOA) is controlled by a private key. It can start transactions; it holds a balance; classically, it runs no code. A contract account has no private key. It cannot start anything on its own — it only reacts, running its code when someone calls it. As my notes put it: with a contract, the code decides what happens to the money in the account, not a person.

Both kinds are the same shape underneath, and the shape is four fields.

02 · Four fields

Every account on Ethereum — yours, a contract's, an exchange's cold wallet — is exactly four values, RLP-encoded into one short string: [nonce, balance, storageRoot, codeHash]. That string is the row. Below are three real accounts pulled from the chain; toggle between them and hover each field. Two of the four hashes are recomputed in your browser and checked against what the node returned:

Things worth watching for:

  • nonce is a counter of how many transactions this account has sent — it's what stops you from broadcasting the same signed payment twice. For a contract, it counts how many other contracts it has deployed.
  • balance is in wei. One ETH is 10^18 wei — not 10^9, a number my own notes fumble in a later chapter, so I'm nailing it here. The balance is stored as a trimmed big-endian byte string, which is why RLP has no fixed-width integers: zero is the empty string, and there are no leading zeros to waste.
  • codeHash is the whole EOA-vs-contract distinction, as one hash. For a plain EOA it is keccak256 of the empty string — c5d2…a470 — the exact same hash for every keyless account on Earth. Hover it: the figure computes keccak256("") live and it lands on the captured value. For the contract (WETH9), it's the hash of 3,124 bytes of real bytecode, and the figure re-hashes that bytecode to prove the number isn't decoration. Because codeHash is fixed at deployment and never rewritten, a contract's code is immutable — a property that falls straight out of the data structure.
  • storageRoot is a 32-byte hash standing in for this account's own private key-value store — a whole separate tree of 2^256 possible slots. An EOA stores nothing, so its storageRoot is the empty-trie root 56e81f17…, another universal constant the figure recomputes. The contract's is a real root the node reported, and I can't rebuild it here without the entire storage trie — so I show it exactly as captured and say so. That trie is the next post.

The third tab is the honest asterisk on "EOAs run no code." Since EIP-7702 shipped in 2025, an EOA can point at a contract's code and execute it while still being an ordinary key-controlled account. The tab shows a real one: its codeHash isn't empty, it's the hash of a 23-byte 0xef0100‖address designator that says "run that contract's code as if it were mine." The four-field structure didn't change. The comfortable rule "an EOA never has code" did. I'd rather show you the exception than teach you a 2021 invariant that 2026 quietly broke.

03 · An address you can compute before it exists

Contracts don't choose their addresses, and nobody assigns them. An address is derived, deterministically, from two things and nothing else: who is deploying, and how many times they've deployed before. The formula is keccak256(rlp([sender, nonce])), keep the last 20 bytes. No bytecode enters into it — you can know a contract's address before you've written a line of its code. Type a sender and a nonce and watch all three steps recompute:

Things worth watching for:

  • The RLP is tiny and legible. A 20-byte sender wraps to a 21-byte string (0x94 means "twenty bytes follow"), the nonce is a trimmed integer, and a two-byte list header wraps the pair. That's the entire preimage. Keccak turns it into 32 bytes; the address is the low 20, and the top 12 are simply thrown away.
  • It matches a real vector. The default sender and nonces 0–3 reproduce the classic go-ethereum CreateAddress test vectors exactly, digit for digit — the same numbers baked into Ethereum's own test suite. Change the sender and you're computing a genuine address you could deploy to.
  • The nonce rule has a wrinkle worth stating. A brand-new EOA's first contract uses nonce 0; the account's nonce is then 1. But a contract account's own nonce starts at 1, not 0 (a rule called EIP-161 fixed this in 2016 — and yes, my notes say 0, and my notes are wrong). Either way the address always uses whatever the nonce is at the instant of creation, which is why deploying the same bytecode from the same account twice gives two different addresses.

This is the deepest structural break from Bitcoin in the whole post. A Bitcoin address is a hash of a public key — it commits to who can spend. An Ethereum contract address is a hash of its own origin story. One names an owner; the other names a birth.

04 · The header commits to three roots

Now zoom back out to the block, and the contrast with part one lands in one glance. A Bitcoin header commits to exactly one thing about its contents: merkle_root, a hash over the block's transactions. That's all a Bitcoin block has to vouch for, because a block is just a batch of payments. An Ethereum header commits to three roots, and the extra two are the shape of the whole machine. Toggle between the two headers — both real, both live — and hover the lit fields:

The three roots, and what each is for:

  • stateRoot is the one with no Bitcoin equivalent at all. It's the root hash of the entire world — every account's four-field row, all at once — after this block's transactions have run. Change one balance in one account and this single 32-byte number changes. This is the sense in which the header commits to a database: 32 bytes vouch for the complete state of the machine.
  • transactionsRoot is the closest thing to Bitcoin's merkle_root: a commitment to the list of transactions in this block.
  • receiptsRoot has no Bitcoin analogue either, because Bitcoin never recorded what a transaction did. A receipt holds a transaction's outcome — did it succeed, how much gas it burned, and the event logs it emitted. Ethereum commits to results, not just inputs.

So a block is best read as a state transition function: take the parent block's stateRoot, apply the transactions under transactionsRoot, and you get this block's stateRoot, with receiptsRoot recording what happened on the way. The parent committed to a different state; the block is the diff, and the header is the notarized before-and-after.

Three honest disclaimers live in that figure. First, I keep calling them "the original three" because a 2026 header carries more: withdrawalsRoot (validator withdrawals, 2023), parentBeaconBlockRoot (a link to the proof-of-stake consensus layer, 2024), and requestsHash (2025). Six hash commitments in one header, where Bitcoin has one. Second — and this is the big correction — look at difficulty (zero), nonce (eight zero bytes), and mixHash. My notes describe Ethereum mining, difficulty, and uncle blocks at length. All of that is gone. Ethereum has been proof-of-stake since the Merge in September 2022; there is no mining, no difficulty, no nonce to grind. Those fields survive as vestigial zeros for backwards compatibility, and mixHash was quietly repurposed to carry the beacon chain's randomness. Third: this is a real header from a real node, but the accounts in section 02 are the honest part of the state and the full world-state trie is far too large to ship — what I show you is real rows, not the whole database.

05 · What's next

The mental model, compressed. Ethereum is one enormous table, keyed by 20-byte address, holding one four-field row — [nonce, balance, storageRoot, codeHash] — per account. EOAs are keys; contracts are code; the difference is one hash. You don't spend coins, you mutate rows, and a block header commits to the entire table's root in 32 bytes, so the chain of headers you already understand from Bitcoin is now a chain of database snapshots. If Ethereum is a global decentralized computer, that table is its hard drive.

Which leaves exactly one thing unexplained, the thing every root in section 04 was quietly a hash of: how do you take a table of a hundred million accounts and reduce it to a single 32-byte number that changes the instant any row changes, cheaply, without rehashing the world? That structure is a Merkle Patricia Trie — a Merkle tree crossed with a radix tree — and it is so central that Ethereum is best described as this one data structure with a blockchain bolted on for durability. It's the next post: the state is a tree of trees.

Same promise as the Bitcoin half: it's bytes all the way down, and you can read every one of them.

$git log --oneline public/posts/ethereum-state/
536605dNeural networks from scratch: a third post on pixels and vectorisationtoday
© 2026 · v2.0 · santiago toscanini