Day 21 · Accounts vs UTXO
In Project 5 the ledger had no accounts. Bitcoin tracks coins — unspent
transaction outputs — and “your balance” is just the sum of the UTXOs your keys can unlock. There is no
row anywhere that says alice: 50; there is only a pile of coins, each spendable exactly once. That model
is beautiful for money and useless for a computer. Today we build the other model — Ethereum’s
account — and the contrast is the lesson: a world computer needs somewhere to remember things, and
UTXOs are designed to be forgotten.
Two ledgers, two mental models
Section titled “Two ledgers, two mental models”Picture moving 30 units from Alice to Bob in each system.
UTXO (Bitcoin) Account (Ethereum) ───────────── ────────────────── inputs: Alice's 50-coin alice.balance: 50 → 20 outputs: Bob 30, Alice 20 (change) bob.balance: 0 → 30 the 50-coin is destroyed, the same two rows are two new coins are created edited in placeIn the UTXO world a transaction consumes whole coins and creates new ones; the old coin ceases to exist and Alice gets “change” back as a fresh coin. Nothing is mutated — coins are immutable, created once and spent once. In the account world there is a map from address to a mutable record, and a transfer simply edits two numbers. That difference cascades into everything:
| UTXO (Bitcoin) | Account (Ethereum) | |
|---|---|---|
| State is… | a set of unspent coins | a map address → account |
| A transfer… | destroys + creates coins | edits balances in place |
| ”Balance” is… | derived (sum your UTXOs) | stored directly |
| Persistent memory? | none — coins are spent once | yes — storage per account |
| Replay protection | inherent (a coin is gone once spent) | needs an explicit nonce |
| Parallelism | easy (independent coins) | harder (shared mutable map) |
Neither is “better” — they are different tools. UTXO’s statelessness makes Bitcoin simpler to reason about and easier to parallelise. But a contract that keeps a running total, or a token whose balances change, needs a cell that persists and mutates between transactions. The account model gives you exactly that, and pays for it with the complications in the right-hand column — chiefly, the need to invent replay protection from scratch.
The account record
Section titled “The account record”Here is the value half of Ethereum’s state map, as ethmini models it. Four fields, and the whole “world
computer” idea is hiding in which four:
pub struct Account { pub nonce: u64, // how many txs this account has sent pub balance: u128, // native currency, smallest unit pub storage: BTreeMap<u64, u64>, // the contract's persistent memory pub code: Vec<u8>, // the contract's bytecode (empty for a wallet) }balanceis the obvious one: native currency. Real Ethereum counts wei (1 ether = 10^18 wei) in a 256-bit integer; we use au128, which is plenty and keeps arithmetic ordinary.codeis what makes this a computer. Ifcodeis empty, the account is a plain wallet — an externally owned account (EOA). Ifcodeis non-empty, the account is a contract, and sending it a call runs that code. The presence of bytecode is the only thing distinguishing a program from a purse.storageis the contract’s private, persistent key/value memory — the cell that survives between calls. This is the field UTXO has no equivalent for, and the reason the account model exists.nonceis the subtle one, and it’s today’s main event.
Nonces: replay protection you have to build
Section titled “Nonces: replay protection you have to build”Here is a problem the UTXO model never has. Alice signs “pay Bob 30” and broadcasts it. In Bitcoin, that transaction names specific coins as inputs; once mined, those coins are spent, and rebroadcasting the same transaction is a no-op — the inputs no longer exist. Replay protection is inherent in the model.
In the account model, “pay Bob 30” is just edit two balances. Nothing about it is consumed. If Bob (or a network eavesdropper) rebroadcasts Alice’s signed transaction ten times, why shouldn’t it run ten times and drain her account? The signature is still valid every time.
The fix is the nonce: a per-account counter, starting at 0, that a transaction must name, and that applying the transaction increments.
alice.nonce = 0
tx#1: { from: alice, nonce: 0, pay bob 30 } ✓ (0 == 0) → alice.nonce = 1 replay the SAME tx: nonce: 0 ✗ (account is at 1, tx says 0) REJECTED tx#2: { from: alice, nonce: 1, pay bob 10 } ✓ (1 == 1) → alice.nonce = 2A transaction is valid only if its nonce equals the sender’s current nonce. A stale nonce (“0” when the
account is at “1”) means “already spent” and is rejected; a future nonce (“5” when the account is at “1”)
means “out of order” and is also rejected. This is exactly the check at the top of WorldState::apply:
if sender.nonce != tx.nonce { return Err(EthError::NonceMismatch { addr: tx.from, expected: sender.nonce, got: tx.nonce, }); } // ...later, on inclusion: self.account_mut(tx.from).nonce += 1;The nonce does double duty: it’s replay protection and a strict ordering. Because each account’s transactions must be applied in nonce order 0, 1, 2, …, there is never ambiguity about which of two conflicting transactions “happened first” from one sender.
Under the hood — rejection vs. revert, and why the nonce always advances
Section titled “Under the hood — rejection vs. revert, and why the nonce always advances”There are two ways a transaction can fail, and conflating them is a classic bug. ethmini keeps them
strictly apart, and the nonce is what reveals the difference:
- A rejection (
Err(EthError)) — wrong nonce, unknown sender, can’t afford the value — means the transaction was never valid to include. The world state does not move at all. The nonce does not advance, because nothing happened. - A revert (
Ok(Receipt { status: Reverted })) — a contract call that ran but threw (out of gas, say) — means the transaction was included, mined into history, and its sender genuinely paid for the attempt. Its state changes are rolled back, but the nonce still advances. Otherwise a transaction that reverts could be replayed forever.
You’ll build that revert path on Day 23. For today, hold the principle: a transaction that makes it into the chain consumes its nonce whether or not it succeeds — because the nonce isn’t recording success, it’s recording that this exact signed message has been used up.
Why a world computer needs mutable shared state
Section titled “Why a world computer needs mutable shared state”Step back to the project’s question: what problem did Ethereum solve? It wanted programs that live on a shared ledger — a token, an exchange, a vote — and a program is nothing without memory. The whole point of a contract is that call #2 can see what call #1 did. That requires a cell of state that:
- persists between transactions (it’s the
storagemap, saved in the world, not in any process), and - is shared — every node and every user sees and agrees on the same value, and
- mutates — transactions change it in place.
UTXO offers none of these by design. So the account model is not a stylistic choice; it is the minimum
viable substrate for programmability. And the instant state is both shared and mutable, you inherit every
hard problem in this playbook at once — determinism, atomicity, ordering, and (next door, in Rust) safe
mutation. That is the bridge to the build: what does this force you to understand, and what is the
compiler protecting you from? Making Account a plain owned struct in a HashMap means Rust’s borrow
checker won’t let two parts of apply hold conflicting mutable references into the map — the same “shared
XOR mutable” rule from The Rust Mindset that protected kvlite’s
HashMap now guards the world state, so a state transition can’t accidentally alias and corrupt the very
account it’s editing.
Tomorrow we assemble these accounts into the full world state and reduce it to a single hash.
→ Next: Day 22 · World State & Roots · Back to the Project 6 overview
Check your understanding
Section titled “Check your understanding”- In one sentence each, contrast how a UTXO chain and an account chain represent “Alice’s balance” and how each changes it during a transfer.
- Bitcoin needs no nonce but Ethereum does. What property of the UTXO model gives Bitcoin replay protection for free, and why does the account model lose it?
- Walk through what happens when Alice’s account is at nonce 3 and she submits a transaction claiming nonce 1. Is it a rejection or a revert? Does her nonce change?
- Which single field of
Accountturns it from a wallet into a contract, and which field gives a contract the persistent memory that UTXOs can’t provide? - The page claims a world computer needs mutable shared state. Name the three properties contract storage must have, and explain why a UTXO set provides none of them.
Show answers
- UTXO: a balance is derived — the sum of the unspent coins your keys can unlock; a transfer destroys whole coins and creates new ones (paying yourself “change”). Account: a balance is stored directly in the account record; a transfer edits two balance numbers in place.
- In UTXO, a transaction names specific coins as inputs and consumes them; once spent, those coins no longer exist, so rebroadcasting the transaction does nothing — replay protection is inherent. An account transfer just edits balances and consumes nothing, so the same signed transaction would be valid (and re-runnable) again and again — hence the explicit nonce.
- The transaction’s nonce (1) doesn’t equal the account’s current nonce (3), so
applyreturnsErr(EthError::NonceMismatch). It is a rejection, not a revert: the world state is untouched and the nonce does not change — the transaction was never included. codeturns an account into a contract (a non-empty bytecode field; an empty one is a plain externally owned account).storagegives the contract its persistent key/value memory that survives between calls.- Contract storage must persist between transactions, be shared (every node agrees on the same value), and mutate in place. A UTXO set provides none: coins are created once and spent once (no persistence of an editable cell), and there is no per-program memory to share or mutate — by design, the model is meant to forget.