Skip to content

Accounts and Addresses: A Mutable Ledger Instead of Coins

The part overview framed the goal: we are building a world computer from scratch, one Rust type at a time, and asking at every step what it costs untrusting strangers to agree on the result. Before a computer can run anything, it needs somewhere to keep state. That is this page. We define the two nouns everything else rests on — the address that names a thing on the ledger, and the account that thing points to.

The single most important decision here is how we model who owns what. Bitcoin answered that with coins; Ethereum answers it with a mutable map. That difference is not cosmetic — it is the reason Ethereum can host programs at all, and it drags in one hard new problem (replay protection) that Bitcoin gets for free. By the end of this page you will have typed the real Address and Account types from ethmini, our companion crate, and understood why each has exactly the fields it has.

Suppose Alice wants to move 30 units to Bob. There are two fundamentally different ways a ledger can represent that, and the whole design of a blockchain follows from which you pick.

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 PLACE

In the UTXO model (which the Bitcoin book builds), the ledger is a set of unspent coins. There is no row anywhere that reads alice: 50. There is only a pile of coins, each unlockable by some key and each spendable exactly once. A transfer consumes whole coins as inputs and mints new coins as outputs — Alice pays herself the leftover 20 as “change.” Nothing is mutated; coins are immutable, created once and spent once. Your “balance” is a derived quantity: sum the coins your keys can unlock.

In the account model, the ledger is a map from address to a mutable record. Alice’s balance is a number stored directly in her account, and a transfer just edits two numbers. Nothing is created or destroyed — two existing rows change.

That single choice cascades into everything:

UTXO (Bitcoin)Account (Ethereum)
State is…a set of unspent coinsa map address → account
A transfer…destroys + creates coinsedits balances in place
”Balance” is…derived (sum your coins)stored directly
Persistent per-owner memory?none — coins are spent onceyes — a storage cell per account
Replay protectioninherent (a spent coin is gone)must be built (a nonce)
Parallelismeasy (independent coins)harder (one shared mutable map)

Neither column is “better.” UTXO’s statelessness makes Bitcoin simpler to reason about and easier to parallelise — that is a real virtue for money. But a program — a token whose balances change, an exchange that holds funds, a vote that tallies — needs a cell that persists and mutates between transactions. UTXO is designed to forget; a computer is defined by its memory. The account model is the minimum substrate that gives a program somewhere to remember, and it pays for that with the complications in the right-hand column.

Every entry in the world-state map is keyed by an address. In ethmini — and in real Ethereum — an address is exactly 20 bytes:

/// A 20-byte account address.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Address(pub [u8; 20]);

Twenty bytes is 160 bits, and it is not an arbitrary number. Real Ethereum addresses are the last 20 bytes of a 32-byte Keccak-256 hash — of a public key (for a wallet) or of the creator’s address and nonce (for a contract). Truncating a 256-bit hash to 160 bits keeps addresses short enough to type and print while leaving the space astronomically large: 2^160 possible addresses means two independently generated addresses colliding is not a risk anyone plans around.

ethmini keeps the real 20-byte width but simplifies two honest details, so you always know where the model diverges from the chain:

  • We derive contract addresses with SHA-256 instead of Keccak-256. The shape is real; the hash function is a simplification we own up to.
  • We let tests mint readable addresses with a helper, so 0x000…0001 is a valid demo address you can reason about by eye:
/// Build a readable address from a small integer — handy for tests and demos.
/// The integer lands in the last 8 bytes, big-endian, so the hex reads as expected.
pub fn from_low_u64(n: u64) -> Self {
let mut bytes = [0u8; 20];
bytes[12..20].copy_from_slice(&n.to_be_bytes());
Address(bytes)
}

Notice the derived traits on Address, because two of them are load-bearing later. Hash lets an address be a HashMap key — that is how the world state looks accounts up. Ord/PartialOrd let addresses be sorted, which sounds cosmetic but is not: when we hash the whole world into a single state root, we must walk the accounts in a deterministic order, and a HashMap’s iteration order is deliberately randomised. Sorting by address is what makes the root reproducible across every node. We defined the type here; the ordering pays off two pages from now.

The Account: four fields, and why each earns its place

Section titled “The Account: four fields, and why each earns its place”

An address names a thing. Here is the thing itself — the value half of the world-state map:

/// One account's full record — the value half of the world-state map.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Account {
pub nonce: u64, // how many txs this account has sent
pub balance: u128, // native currency, in the smallest unit
pub storage: BTreeMap<u64, u64>, // the contract's persistent memory
pub code: Vec<u8>, // the contract's bytecode (empty for a wallet)
}

Four fields, and the entire “world computer” idea is hiding in which four. Take them in order of subtlety.

The native currency this account holds, counted in the smallest unit. Real Ethereum counts wei in a 256-bit integer, where 1 ether = 10^18 wei; that width exists so balances share the natural word size of Keccak hashes and elliptic-curve math. We use a u128, which is plenty for a teaching chain and keeps the arithmetic ordinary (it wraps at 2^128 instead of 2^256 — a seam we note rather than hide). A transfer is nothing more than decrementing one account’s balance and incrementing another’s. That is the whole magic trick the account model performs, and it is why “your balance” is a stored number, not a derived sum.

code — the field that makes this a computer

Section titled “code — the field that makes this a computer”

code holds bytecode. If it is empty, the account is a plain wallet — Ethereum calls it an externally owned account (EOA), meaning it is controlled by a private key out in the world, not by on-chain logic. If code is non-empty, the account is a contract, and sending it a call runs that bytecode on the tiny stack VM we build later.

That is the entire distinction. There is no is_contract flag stored on the chain, no separate contract table. The presence of code is the only thing that tells a program from a purse:

impl Account {
/// Does this account hold contract code? If so, sending a call to it *runs*
/// that code; if not, a call is just a value transfer.
pub fn is_contract(&self) -> bool {
!self.code.is_empty()
}
}

is_contract is one line because the model is honest: “contract” is not a type, it is a state an account is in. The same 20-byte address space holds both wallets and programs, and whether a call transfers value or executes logic is decided by looking at one field.

storage is the contract’s private, persistent key/value memory — the cell that survives between calls. This is the field the UTXO model has no equivalent for, and it is the reason the account model exists at all. A program is nothing without memory; the whole point of a contract is that call #2 can see what call #1 wrote. That requires a cell that persists (it lives in the world, not in any short-lived process), is shared (every node agrees on its value), and mutates in place. Coins offer none of the three.

Real EVM storage maps 256-bit words to 256-bit words. ethmini uses u64 → u64 and, crucially, a BTreeMap rather than a HashMap — because a BTreeMap iterates in sorted key order, so it serialises to the same bytes every time. A deterministic state root demands deterministic bytes, and that requirement reaches all the way down into the choice of container.

The nonce is a per-account, strictly-increasing counter, starting at 0, bumped once each time the account sends a transaction. It looks like bookkeeping. It is actually the price the account model pays for giving up UTXO’s free replay protection, and it quietly does two different jobs. We preview both here and prove them out on the state-transition page.

Job one — replay protection. In UTXO, a transaction names specific coins as inputs; once mined, those coins are spent, so rebroadcasting the same signed transaction is a no-op — the inputs no longer exist. In the account model, “pay Bob 30” is just edit two balances, and nothing about it is consumed. If Bob (or a network eavesdropper) rebroadcasts Alice’s still-valid signature ten times, why shouldn’t it drain her account ten times? The nonce is the answer: a transaction is valid only if its nonce equals the sender’s current nonce, and applying it increments the counter, so the same signed message can never be replayed.

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) REJECTED
tx#2: { from: alice, nonce: 1, pay bob 10 } ✓ (1 == 1) → alice.nonce = 2

Job two — seeding contract addresses. When an account creates a contract, the new contract’s address is derived from the creator’s address and its current nonce. Because the nonce strictly increases, each creation yields a fresh, collision-free address without any global registry. The same counter that stops replays also hands out contract addresses. We use it that way when we deploy a contract.

Under the hood — why a plain owned struct in a map

Section titled “Under the hood — why a plain owned struct in a map”

Account is a plain owned struct, and the world state is a HashMap<Address, Account>. That choice is doing safety work for us. Because the accounts are owned by the map rather than shared behind references, Rust’s borrow checker will not let two parts of a state transition hold conflicting mutable references into the map at once. The “shared XOR mutable” rule — the same rule that guards any Rust HashMap — now guards the world state, so a transfer physically cannot alias and corrupt the very account it is editing.

Default matters too: Account::default() is the zero account — nonce 0, zero balance, empty storage, empty code. An address that has never been touched is the default account, which means “does this account exist yet?” and “is this account empty?” collapse into the same question. with_balance is just a thin constructor over that default, changing exactly one field:

impl Account {
/// A fresh, empty externally-owned account with a starting balance.
pub fn with_balance(balance: u128) -> Self {
Account {
balance,
..Account::default()
}
}
}

You will call Account::with_balance(...) constantly to fund test accounts, and is_contract() every time the VM decides whether a call executes code or merely moves value.

  • Why does it exist? Because a shared computer needs shared, addressable memory, and a set of spend-once coins cannot provide it. The account model exists to give programs a mutable cell — storage — that persists between transactions and that every node agrees on.
  • What problem does it solve? It turns “who owns what” from a pile of immutable coins into a map: address → account, so a balance is a number you edit in place and a contract is a record that remembers. That is the substrate programmability requires.
  • What are the trade-offs? You gain mutable, persistent, shared state — and inherit replay protection (the nonce), harder parallelism (one shared mutable map instead of independent coins), and a strict determinism burden (sorted storage, sorted accounts) to keep the state root reproducible.
  • When should I avoid it? When your ledger only needs to move value and never run programs. Pure payment systems are simpler, more parallel, and easier to audit as a UTXO set — reaching for accounts there buys complexity you will not use.
  • What breaks if I remove it? Everything above money. Without a mutable per-account record there is no storage, so no contract can remember anything across calls — the world computer degrades back into a coin ledger, and the rest of this part has nothing to compute over.
  1. 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.
  2. An Address is 20 bytes. Where do those 20 bytes come from in real Ethereum, and why does ethmini derive Ord on the type even though addresses are just keys?
  3. Name the four fields of Account. Which single field distinguishes a contract from a wallet, and how does is_contract use it?
  4. Bitcoin needs no nonce but Ethereum does. What property of UTXO gives Bitcoin replay protection for free, and what two jobs does the nonce do in the account model?
  5. Why does storage use a BTreeMap rather than a HashMap, and what does that have to do with agreement between untrusting nodes?
Show answers
  1. UTXO: a balance is derived — the sum of the unspent coins your keys can unlock — and a transfer destroys whole coins and creates new ones (paying yourself “change”). Account: a balance is stored directly in the account record, and a transfer decrements one balance and increments another in place.
  2. Real Ethereum addresses are the last 20 bytes of a Keccak-256 hash — of a public key (wallet) or of the creator’s address and nonce (contract). ethmini derives Ord so accounts can be sorted by address; iterating the world in a deterministic order is what makes the state root reproducible across nodes, since a HashMap’s order is randomised.
  3. nonce, balance, storage, code. code distinguishes a contract from a wallet: a non-empty code field means the account runs bytecode on a call, an empty one is a plain externally owned account. is_contract returns !self.code.is_empty().
  4. In UTXO a transaction consumes specific coins as inputs, so once they are spent the transaction can never re-run — replay protection is inherent. An account transfer consumes nothing, so the nonce supplies it explicitly. The nonce’s two jobs: replay protection (a tx is valid only if its nonce equals the sender’s current nonce, which then increments) and seeding contract addresses (a new contract’s address derives from the creator’s address plus its current nonce).
  5. A BTreeMap iterates in sorted key order, so it serialises to the same bytes every time; a HashMap’s order is randomised. A reproducible state root demands deterministic bytes, and every node must independently arrive at the same root to agree on the state — so the container choice reaches all the way down from the consensus requirement.