Skip to content

Revision: From Accounts to a Running World Computer

We started this part with a ledger that could only track coins and ended it with a machine that runs programs. Along the way we never added magic — every capability came from one more small, honest piece bolted onto the last. This page walks back through that build in one breath, in the order we constructed it, so you can see the whole dependency chain as a single object rather than seven separate pages.

The throughline for the whole book is one question: how do untrusting strangers agree on the state of a shared world computer — and what does it cost to run one? This part answered the first half with a state root and the second half with gas. Everything else is scaffolding to make those two words mean something concrete. Let us retrace it.

Each piece exists only because the one below it does. Nothing here is arbitrary; pull a layer out and the layers above it collapse.

deploy & call ── a contract is bytecode in an account, run on demand
gas ── every VM step costs; the meter is what bounds untrusted code
stack VM ── a gas-metered loop over bytecode against a storage map
state root ── one hash fingerprinting the entire world; strangers compare it
apply (STF) ── the rule that turns stateₙ into stateₙ₊₁ for one transaction
accounts ── a mutable map: address → { nonce, balance, storage, code }

Read it bottom to top and it is the story of the part. The account is the noun; apply is the verb; the state root is the thing strangers agree on; the VM is what makes an account do something; gas is what makes running it safe; and deploy-and-call is the payoff where all five click together.

Everything rests on the shift we made on Accounts and Addresses. Bitcoin tracks coins — outputs created once and destroyed once, with no memory between transactions. We threw that out and tracked accounts instead: a map from a 20-byte Address to a record you edit in place.

pub struct Account {
pub nonce: u64, // replay protection + contract-address seed
pub balance: u128, // native currency, smallest unit
pub storage: BTreeMap<u64, u64>, // the contract's persistent memory
pub code: Vec<u8>, // bytecode; empty for a plain wallet
}

Four fields, each load-bearing. The balance is the money. The storage is the memory that lets a program remember things between calls — the single feature that separates a world computer from a calculator. The code is what makes an account a contract: is_contract() is defined as nothing more than “the code field is non-empty.” A wallet is just an account with no code.

And the nonce — a per-account counter, bumped once per transaction sent — is quietly doing the hardest job of all. It is replay protection. A signed transaction names the sender’s current nonce; applying it bumps the nonce by one; re-broadcasting the same transaction now names a stale nonce and is refused. Without it, a stranger who saw your transaction on the wire could send it again and drain you. We will see in a moment that the nonce is also the one piece of state a reverted transaction still consumes — which is exactly what stops a failed transaction from being replayed for free.

World state and apply: the state-transition function

Section titled “World state and apply: the state-transition function”

A blockchain is formally a state machine: a current state plus a rule for producing the next state given a transaction. That rule is WorldState::apply, the subject of World State and the State-Transition Function. Everything blocks, consensus, networking exists only to agree on which transactions to feed this function and in what order. The function itself is small and local.

stateₙ ──apply(tx)──▶ stateₙ₊₁
│ │
state_root state_root'

apply runs in two phases. First validation, where any failure rejects the transaction and leaves the world byte-for-byte unchanged: the sender must exist, the nonce must match, and the balance must cover the value being moved. All three checks happen before a single byte of state is touched. Then, if validation passes, apply snapshots the accounts, bumps the sender’s nonce unconditionally, and runs the transaction’s effect — a transfer, a deploy, or a call.

This is where the two-kinds-of-failure distinction is born, and it is worth stating precisely because it is the most misunderstood idea in the whole part.

There are exactly two ways a transaction can fail, and they are not the same:

  • Rejection returns Err(EthError). The transaction was never valid to include — unknown sender, wrong nonce, unaffordable value. The world state does not move at all. It is as if the transaction never existed; it is not part of history, and it consumes nothing.
  • Revert returns Ok(Receipt { status: Reverted, .. }). The transaction was valid and is included — but the contract call it triggered blew up mid-execution. Its state changes are rolled back to the pre-transaction snapshot, but the sender’s nonce stays consumed. The transaction is in history as a failure.
EthError (rejection) VmError (revert)
───────────────────── ──────────────────
never valid to include valid, included as a failure
state 100% untouched state rolled back to snapshot
nonce NOT consumed nonce IS consumed
not in history in history, marked Reverted

That “nonce IS consumed on revert” line is the crux. It is why a transaction that runs out of gas cannot simply be resubmitted for free — it already burned its nonce, and it burned its gas. The revert mechanism itself is beautifully plain: apply clones the account map before executing, and on a thrown VmError it restores the clone and then re-applies only the nonce bump. All-or-nothing state, minus the one thing that must survive.

The state root: one hash for the whole world

Section titled “The state root: one hash for the whole world”

Now the payoff for the throughline’s first half. On The State Root we asked: how do untrusting strangers agree they are looking at the same world without sending each other the entire world? The answer is a state root — a single 32-byte hash that fingerprints every account, balance, nonce, storage slot, and byte of code at once.

Two nodes that ran the same transactions in the same order produce the same root. If even one storage slot differs, the roots differ, and each side knows instantly that they disagree — without diffing gigabytes of state. That single hash is the artifact strangers actually compare. It is the technical meaning of “agree on the state of a shared world computer.”

Our implementation is deliberately honest about being a simplification. We sort the accounts by address (because a HashMap’s iteration order is randomized, and a reproducible hash demands canonical bytes), serialize the sorted pairs, and hash them with SHA-256. That sort-and-hash gives the property — change anything, the root changes; change nothing, it stays identical — but not the efficiency. Real Ethereum stores state in a Merkle-Patricia trie, which yields the same commitment plus cheap proofs of individual accounts and incremental updates when one slot changes. Our flat scheme has to re-hash the whole world every time. The idea is real; the data structure is the shortcut.

The stack VM: making an account do something

Section titled “The stack VM: making an account do something”

Up to here, an account can hold code but nothing runs it. A Tiny Stack VM built the interpreter that does. A contract’s code is a flat Vec<u8> of bytecode — exactly how real Ethereum stores it — and the VM is a loop that decodes one byte into an opcode, executes it against a stack and a storage map, and moves to the next.

bytecode: [ PUSH 0 | SLOAD | PUSH 1 | ADD | PUSH 0 | SSTORE | STOP ]
│ │ │ │ │ │ │
loop: decode → charge gas → execute against (stack, storage) → advance pc

The stack holds transient operands. PUSH puts a word on it; ADD pops two and pushes their sum; SLOAD reads a storage slot onto the stack; SSTORE writes the top of the stack into storage. The storage writes are the only ones that persist — they outlive the call and change the account’s state, and therefore the state root. Everything on the stack evaporates when the call ends.

The opcode numbers deliberately match real EVM (ADD is 0x01, SSTORE is 0x55, RETURN is 0xf3), so the bytecode you assemble here would be recognizable to anyone who has read the yellow paper. Jumps are checked against a pre-computed set of JUMPDEST positions, so you can never jump into the middle of an instruction or off the end of the code — the same jump-destination analysis real EVM does. The VM runs against a copy of storage and only hands the new copy back on a clean halt; if anything throws, the caller keeps the original. That copy-and-commit is the revert mechanism from apply, in miniature.

Gas: pricing every step so strangers can run your code

Section titled “Gas: pricing every step so strangers can run your code”

The VM as described has a fatal flaw: what stops someone deploying an infinite loop that every node on Earth is then obligated to run forever? Gas is the answer, and it is the throughline’s second half made concrete. Every opcode costs gas; the caller supplies a fixed budget; every op pays before it acts; and when the meter hits zero the machine halts and throws OutOfGas.

pub fn gas_cost(&self) -> u64 {
match self {
Op::Stop | Op::Return => 0,
Op::JumpDest => 1,
Op::Push(_) | Op::Pop | Op::Add | Op::Sub => 3,
Op::Mul => 5,
Op::Jump => 8,
Op::JumpI => 10,
Op::SLoad => 100,
Op::SStore => 200,
}
}

Notice the ordering of those costs, which is the real lesson: arithmetic is cheap, a storage read is much dearer, and a storage write is dearest of all — because a write grows the state that every full node must keep forever. Gas prices work in proportion to the burden an operation puts on the shared machine.

This is also how we dodge the halting problem without solving it. We cannot decide in advance whether arbitrary bytecode terminates. We do not have to: we simply cap how much it may run. A loop that would run forever instead runs until its gas is gone, then halts and reverts. That is the answer to “what does it cost to run a shared world computer” — a finite, per-transaction price that makes running anyone’s code safe to accept.

Deploying and calling: bytecode that remembers

Section titled “Deploying and calling: bytecode that remembers”

The final page, Deploying and Calling a Contract, spent nothing new — it just clicked all five earlier pieces together. A deploy transaction creates a fresh account, funds it, and writes the bytecode into its code field. Its address is derived deterministically from the creator and the creator’s nonce (sha256(creator ‖ nonce) in our chain, keccak256(rlp([sender, nonce])) in real Ethereum), so the same account deploying twice gets two different contracts, and anyone can predict the address in advance.

A call transaction to that account then runs the code: it hands the contract’s code, a copy of its storage, and a gas_limit to the VM, and on a clean halt commits the new storage back. Call the counter three times and you watch two numbers move on every call — the gas burned (312 each, from above) and the state root, which changes precisely because slot 0 changed.

That is the whole thesis in one demo. A smart contract is not a mysterious object. It is a few bytes of bytecode living in an account’s code field, run on demand by a gas-metered loop against that account’s storage map. Strip away the vocabulary and there is nothing else there. The “world computer” is a map of such accounts, and running it means feeding transactions to apply and agreeing on the state root that results.

The honest simplifications, and where each would slot in

Section titled “The honest simplifications, and where each would slot in”

We built the shape of Ethereum, not Ethereum. It is worth remembering exactly what we simplified, because each shortcut maps cleanly to a real component you could drop back in:

  • 256-bit words → 64-bit. Real EVM words and balances are 256 bits; we used u64 words and u128 balances. The arithmetic still wraps — mod 2⁶⁴ instead of mod 2²⁵⁶. Widening the word type is a mechanical change to the VM’s stack and storage.
  • Keccak → SHA-256. We hash with SHA-256 because we already depend on it; real Ethereum uses Keccak-256 for addresses, state root, and contract-address derivation. Swapping the hash function is a one-line change everywhere it is called.
  • Sort-and-hash → Merkle-Patricia trie. Our state root re-serializes and re-hashes the whole world. The trie gives the same commitment plus efficient proofs and incremental updates — it slots in exactly at state_root.
  • from field → signatures and consensus. Our transaction’s from stands in for “whoever a valid signature recovered to.” A real chain recovers the sender from an ECDSA signature, and a peer-to-peer network plus a consensus rule decides which transactions enter the state machine and in what order. That machinery wraps around apply; it does not change it.

Each of those is a real part of Ethereum you now understand the slot for. You built the state machine at the center; the rest of the system exists to feed it agreed-upon transactions and to commit to the state root it produces.

You can now point at any part of a running world computer and name what it does and why it must exist. An account is a mutable record with optional code. apply is the rule that advances state, with rejection and revert as its two failure modes and the nonce as the thing revert cannot undo. The state root is the single hash untrusting strangers compare to agree. The VM is a gas-metered loop over bytecode. Gas is the finite price that makes running untrusted code safe. And a contract is just bytecode in an account that remembers through its storage.

Untrusting strangers agree on one state root; gas is what it costs to run the shared machine that produces it. That is the world computer, and you have now built a small, honest, running one from scratch.

  1. A transaction with a nonce of 5 arrives, but the sender’s account is at nonce 3. Is this a rejection or a revert, and what happens to the world state and the sender’s nonce?
  2. A contract call runs out of gas halfway through writing three storage slots. Which state changes survive, and why can the transaction not be replayed for free?
  3. Why must the accounts be sorted before computing the state root, and what property would break if you hashed them in HashMap iteration order?
  4. In the gas table, SSTORE costs far more than ADD. What real-world cost is that price proportional to, and why is that the right thing to charge for?
  5. Reduced to its parts, what is a smart contract in this chain — and which single Account field determines whether a call runs code or just moves value?
Show answers
  1. It is a rejection (EthError::NonceMismatch), because a nonce mismatch is caught in the validation phase before any state is touched. The world state is left completely unchanged and the sender’s nonce is not consumed — the transaction never becomes part of history.
  2. None of the storage writes survive: a VmError reverts the call to the pre-transaction snapshot. But the sender’s nonce is still consumed, because apply re-applies the nonce bump after restoring the snapshot. Since the nonce advanced, resubmitting the same transaction would now name a stale nonce and be rejected — so a failed transaction cannot be replayed for free.
  3. A HashMap’s iteration order is randomized per run, so hashing accounts in iteration order would produce a different root for the same state on different runs. Sorting by address makes the serialized bytes canonical, so the root is a function of the state and nothing else — which is the entire point of a fingerprint that strangers can compare.
  4. SSTORE is proportional to the burden of permanently growing the shared state that every full node must store forever, whereas ADD only touches transient stack values. Charging most for the operation that enlarges the world aligns the price a caller pays with the lasting cost they impose on everyone running the machine.
  5. A smart contract is a few bytes of bytecode living in an account’s code field, run on demand by a gas-metered loop against that account’s storage map — nothing more. The code field decides the behavior: a call to an account with non-empty code runs that code, while a call to a codeless account is just a value transfer.