Skip to content

Inside an Account: nonce, balance, storageRoot, codeHash

The previous page, Two Kinds of Account: EOAs and Contracts, split every account into two kinds: a wallet controlled by a private key, and a contract controlled by code. That page told you who controls an account. This page opens the account up and asks what is actually stored inside one.

The answer is short, and its shortness is the point. Every Ethereum account — EOA or contract, holding a fortune or empty — is exactly four fields: nonce, balance, storageRoot, codeHash. Nothing more. In the world-state map we build on the next page, the address is the key; these four fields, together, are the entire value. Get why each of the four earns its place and you understand what a programmable account fundamentally is.

Here is the complete account record. Four fields, two purposes: the top two make it a piece of money; the bottom two make it a piece of a computer.

address (20 bytes) ──▶ ┌──────────────────────────────────────────────┐
│ nonce how many txs this account sent │ ← money /
│ balance wei held, as a big integer │ identity
├──────────────────────────────────────────────┤
│ storageRoot hash of this account's storage │ ← program
│ codeHash hash of this account's bytecode │ (contract)
└──────────────────────────────────────────────┘

An EOA fills in the top two and leaves the bottom two at their empty defaults. A contract fills in all four. That single difference — is codeHash empty or not? — is the whole EOA-vs-contract distinction from the last page, now visible as one field.

The rest of this page walks the four fields, one at a time, in the order they earn their keep.

nonce — a per-account transaction counter

Section titled “nonce — a per-account transaction counter”

The nonce is a plain integer: the number of transactions this account has sent, starting at zero. Send your first transaction and the protocol bumps your nonce from 0 to 1; your next transaction must carry nonce 1, then 2, and so on. It only ever counts up, one per sent transaction, and it can never be reused.

That tiny counter does two heavy jobs at once.

Replay protection. Without a nonce, a signed transaction would be a reusable coupon. Alice signs “pay Bob 1 ETH” once; anyone who saw that signed transaction on the network could rebroadcast the identical bytes and drain her account again and again — the signature is still valid, so nothing would stop it. The nonce closes that hole. Because the transaction commits to a specific nonce, and that nonce is consumed the instant the transaction lands, a replay of the same bytes now demands a nonce the account has already moved past. The chain rejects it. The signature proves who; the nonce proves not already, and not again.

Strict per-account ordering. The nonce also forces your transactions into a single, gap-free line. The protocol will only apply your transaction if its nonce is exactly the account’s current nonce — no skipping ahead, no going back. So even if three of your transactions reach the network out of order, the chain can only execute them as 0, 1, 2. This is what lets you queue dependent actions (“first approve, then spend”) and know they cannot be reordered against you.

account.nonce = 5 (five txs already sent)
incoming tx nonce = 4 → REJECT (already used; a replay)
incoming tx nonce = 5 → ACCEPT → account.nonce becomes 6
incoming tx nonce = 6 → WAIT (future; needs 5 applied first)

The exact check — and what happens to a transaction that arrives too early or too late — is the subject of State Transitions; here it is enough to see why the field is in the record at all. One counter buys you both non-replayability and a total order, per account, for free.

A contract account has a nonce too, but it counts something slightly different: the number of contracts this contract has created. It plays the same structural role — a monotone counter that makes the next thing this account produces unique — which is why one field covers both.

The balance is the account’s holding of ether, the native currency, stored directly as a single large non-negative integer. This is the “stored, not derived” idea from the part overview made literal: unlike a Bitcoin balance, which you compute by summing the coins your keys can unlock, an Ethereum balance is just a number sitting in the account row. A transfer edits that number in place — debit the sender, credit the receiver — as we contrasted on Two Ledgers.

The unit is not ether. It is wei, the smallest indivisible unit, where:

1 ether = 1,000,000,000,000,000,000 wei = 10^18 wei

Storing the balance in wei — the smallest unit — rather than in ether is a deliberate choice, and it is the same choice Bitcoin makes with satoshis: integers only, no fractions, ever. Floating-point money is a bug generator; 0.1 + 0.2 != 0.3 is not a property you want anywhere near a ledger that untrusting strangers must agree on to the last unit. By denominating in the tiniest unit, every balance and every transfer is exact integer arithmetic, and every node computes the identical result down to a single wei. Determinism — the thing agreement requires — falls out of choosing integers over decimals.

Because a real balance can be enormous, Ethereum stores it as a 256-bit unsigned integer — room for far more wei than the total ether supply will ever contain. (Our teaching crate uses a 128-bit integer, which is plenty for a demo chain; more on that simplification below.)

storageRoot — a commitment to persistent storage

Section titled “storageRoot — a commitment to persistent storage”

Now we cross from money into computer. The storageRoot is what gives a contract a memory that survives between calls.

Recall from the part overview that a program is useless without somewhere to remember things between operations. A contract needs exactly that: a token contract must remember every holder’s balance; a voting contract must remember the running tally. That memory lives in the account’s persistent storage — a private key/value store, owned by this one account, that persists from one transaction to the next. It is the cell the part overview promised: the thing a UTXO can never be, because a coin is forgotten the instant it is spent.

But storage can be huge — a popular token has millions of holder balances. If the account record embedded the whole store, the record would balloon and every account would be a different size. So Ethereum does not put the storage in the account. It puts a hash of the storage in the account: the storageRoot. This single fixed-size value is a cryptographic commitment to the entire key/value store — change any one stored value and the root changes; leave the store untouched and the root is byte-for-byte identical.

account.storageRoot ──▶ hash over the whole key/value store
slot 0x00 → 0x2a ┐
slot 0x01 → 0xdeadbeef ├─ hashed together ─▶ storageRoot (32 bytes)
slot 0x07 → 0x01 ┘
edit ONE slot ⇒ storageRoot changes (tamper-evident)
edit NOTHING ⇒ storageRoot unchanged (stable, comparable)

This is the pattern the State & Tries part unpacks in full — the storage is organized as a Merkle-Patricia trie, and the root is that trie’s top hash. For now, hold the shape of the idea: the account carries a fingerprint of its storage, not the storage itself. A pure EOA has no storage, so its storageRoot is the empty/default value — the hash of an empty store. The field is present on every account; it is simply empty on a wallet.

codeHash — a commitment to the account’s code

Section titled “codeHash — a commitment to the account’s code”

The fourth field, codeHash, is the exact same trick applied to a different thing: the contract’s bytecode. It is the hash of the account’s code, not the code itself — for the same reason storage is committed by root rather than inlined. Code can be many kilobytes; a hash is a fixed 32 bytes. The record stays small while pointing at arbitrarily large code.

And this field is where the whole two-kinds-of-account distinction finally lives as data:

codeHash == hash(empty) ⇒ no code ⇒ EXTERNALLY OWNED ACCOUNT (a wallet)
codeHash != hash(empty) ⇒ has code ⇒ CONTRACT ACCOUNT (a program)

An EOA’s codeHash is the hash of the empty byte string — the field is present but points at nothing. A contract’s codeHash is the hash of its deployed bytecode. So the entire EOA-vs-contract split from the previous page reduces to a single, checkable predicate: is codeHash empty? When a transaction arrives at an address, the protocol reads this field to decide whether to simply move value (EOA) or to fetch and run the code (contract). One hash, and the account knows what it is.

Under the hood — why hash the storage and code instead of storing them

Section titled “Under the hood — why hash the storage and code instead of storing them”

It is worth being explicit about why two of the four fields are hashes. Three properties fall out of committing-by-hash, and all three are load-bearing for a shared world computer:

  • Fixed size. Every account record is the same handful of bytes regardless of how much storage or code it owns. That keeps the state map uniform and the account-level trie (next part) shallow and predictable.
  • Cheap comparison. To ask “did this account’s storage change between block n and block n+1?” you compare two 32-byte roots, not two million-entry stores. Nodes lean on this constantly to sync and to prove state.
  • Tamper-evidence. Because the root is a cryptographic hash, you cannot alter a stored value or a byte of code without changing the field — and, cascading upward, the whole world-state root. This is the mechanism by which strangers detect a lie about the state, which is the front half of the book’s question.

The account is, in other words, a fixed-size record that points at arbitrarily large things through hashes. That single design choice is what lets the state stay compact and provable even as contracts accumulate megabytes of storage.

Our companion crate, ethmini, models the account with the same four responsibilities but a couple of honest shortcuts, so the code stays readable while the shape stays real:

/// One account's full record — the value half of the world-state map.
///
/// Four fields, and each earns its place.
pub struct Account {
pub nonce: u64, // a per-account tx counter
pub balance: u128, // wei held (real ETH uses u256)
pub storage: BTreeMap<u64, u64>, // persistent key/value memory
pub code: Vec<u8>, // contract bytecode (empty = EOA)
}
impl Account {
/// The only thing that distinguishes a contract from a wallet:
/// does it hold code?
pub fn is_contract(&self) -> bool {
!self.code.is_empty()
}
}

Two simplifications are on display, and both are deliberate:

  1. Real Ethereum uses 256-bit words, not 64- or 128-bit ones — for balances (in wei), for storage keys and values, and for the arithmetic the EVM performs. We shrink these to ordinary Rust integers so the demo compiles and reads plainly; the widths are wrong, but the fields are right.
  2. Real Ethereum commits storage and code by hash — a storageRoot and a codeHash, each 32 bytes — whereas the crate embeds the storage map and the code bytes directly. Embedding is fine for a small in-memory teaching chain; hashing is what keeps a real account record fixed-size while pointing at arbitrarily large storage and code. The crate’s is_contract() check — “does it hold code?” — is exactly the real protocol’s “is codeHash empty?” test, just reading the code directly instead of through its hash.

Keep both simplifications in mind whenever you read the crate: the responsibilities are faithful, the representations are downsized, and we own up to which is which.

The account record is the smallest object in Ethereum, and every field is here on purpose. Viewed at altitude:

  • Why does it exist? Because a shared, mutable, programmable ledger needs a value type for its state map — one object per account that can hold money, resist replay, and carry a program’s memory and code. These four fields are that value type.
  • What problem does it solve? It packages everything the protocol must know about an account into a fixed-size, hashable record: two fields make it money (nonce, balance), two make it a computer (storageRoot, codeHash), and hashing keeps the record small no matter how large the storage or code grows.
  • What are the trade-offs? Storing balances directly (not as coins) buys in-place edits and simple reads, at the cost of the ordering and replay machinery the nonce exists to provide. Committing storage and code by hash buys fixed size and tamper-evidence, at the cost of an extra layer of indirection — you cannot read a value without walking the trie it commits to.
  • When should I avoid it? You do not get to; every account is this record. But when you only need money that moves and never remembers — Bitcoin’s use case — the account model’s nonce and storage machinery are pure overhead, and a UTXO set is the leaner choice, exactly as Two Ledgers argued.
  • What breaks if I remove it? Drop the nonce and signed transactions become replayable coupons and lose their order. Drop balance and there is no native currency to pay for computation. Drop storageRoot and contracts cannot remember anything between calls — no tokens, no markets, no state at all. Drop codeHash and the protocol can no longer tell a wallet from a program, so “code on a ledger” is impossible. Remove any one and the world computer stops being one.
  1. Name the four fields of an Ethereum account record, and state which two make it “money” and which two make it a “computer.”
  2. The nonce does two jobs at once. Name both, and explain how a single incrementing counter provides each.
  3. A balance is stored in wei, not ether. How many wei are in one ether, and why does denominating in the smallest unit matter for agreement between nodes?
  4. storageRoot and codeHash are both hashes rather than the storage or code themselves. Give two concrete properties this “commit by hash” design buys, and say what stays fixed as a result.
  5. Using only the account record, how does the protocol decide whether an incoming transaction to some address is a plain value transfer or a program to run?
Show answers
  1. nonce, balance, storageRoot, codeHash. The top two — nonce and balance — make the account money and identity; the bottom two — storageRoot and codeHash — make it a program (a contract’s memory and its code). Together the four are the entire value half of the world-state map.
  2. Replay protection and strict per-account ordering. Because each transaction commits to a specific nonce that is consumed when it lands, rebroadcasting the same signed bytes demands a nonce the account has already passed, so replays are rejected. And because the protocol only applies a transaction whose nonce equals the account’s current nonce, transactions can only execute in the exact sequence 0, 1, 2, …, giving a gap-free total order.
  3. 1 ether = 10^18 wei (a billion billion). Denominating in the smallest unit means every balance and transfer is exact integer arithmetic with no fractions or rounding — so every node computes the identical result to the last wei, which is what deterministic agreement requires. Floating-point money would let nodes disagree.
  4. Any two of: fixed size (the record stays a small, uniform handful of bytes no matter how large the storage or code), cheap comparison (comparing two 32-byte roots tells you whether storage changed, without walking millions of entries), and tamper-evidence (altering any stored value or code byte changes the hash and cascades up to the state root, so lies are detectable). What stays fixed is the size of the account record, even as it points at arbitrarily large storage and code.
  5. It reads the codeHash. If codeHash is empty (the hash of no code), the account is an EOA and the transaction is a plain value transfer; if codeHash is non-empty, the account is a contract, and the protocol fetches and runs its code. The one field is the entire EOA-vs-contract decision.