Why Every Node Computes the Same Result
The last four pages built a working machine. We have opcodes on a stack of words, four places data can live, a gas meter that halts runaway code, and an execution context that lets one contract call another. Every one of those pages quietly leaned on a property we never named out loud: that the machine, run by two different people on two different computers, produces exactly the same output.
This page makes that property explicit and shows what it costs. The name is determinism, and it is not a nice-to-have — it is the single load-bearing assumption under all of Ethereum. If two honest nodes could ever disagree about what a transaction did, there would be no “the state of Ethereum” to agree on, and the whole edifice of untrusting strangers sharing one ledger would collapse into two ledgers, then four, then noise.
The EVM is a pure function of (state, transaction)
Section titled “The EVM is a pure function of (state, transaction)”Strip away the networking, the blocks, and the peer-to-peer gossip, and the core of Ethereum is one function:
apply : (State, Transaction) ──▶ (State', Receipt)Given a prior world state and a transaction, it produces exactly one next state and one receipt. In ethmini that function is literally a Rust method, WorldState::apply in rust/ethmini/src/state.rs:
pub fn apply(&mut self, tx: &Transaction) -> Result<Receipt> { /* ... */ }For consensus to be possible, apply must be a pure function in the mathematical sense: its output depends only on its two inputs, and on nothing else in the universe. Same prior state, same transaction — same next state, same receipt, on every machine that runs it, forever. Notice what the signature does not contain: no clock, no random seed, no socket, no file handle. There is nowhere for hidden inputs to sneak in. That absence is not an accident; it is the whole design.
We can state the requirement as an equation. Let S be a state and T a transaction. For any two honest nodes i and j:
apply_i(S, T) == apply_j(S, T) ← must hold, alwaysIf that equality ever fails, the two nodes now hold different states. They will hash those states to different state roots, reject each other’s next block, and the chain forks. Determinism is precisely the condition that makes this equality a theorem rather than a hope.
The state root is the tripwire
Section titled “The state root is the tripwire”Recall from the state-and-tries part that the entire world state is committed into a single 32-byte fingerprint, the state root. Every block header carries the state root that results from executing that block’s transactions. When your node receives a block, it re-executes every transaction itself and checks: does my resulting state root equal the one in the header?
block header says: stateRoot = 0x9f3c… my node computes: stateRoot = 0x9f3c… ✓ accept stateRoot = 0x1a08… ✗ reject — someone is wrongThat check is worthless unless execution is deterministic. If the same transactions could yield different roots on different machines, an honest node would reject honest blocks. The state root turns “did you compute the same thing I did?” into a one-comparison test — but only because the thing being compared is guaranteed identical when the inputs are.
What determinism forbids
Section titled “What determinism forbids”A normal program running on your laptop is free to read the clock, call rand(), open a socket, or use a float. The EVM can do none of these, and the reason is always the same: each of them is a source of input that differs from machine to machine or from moment to moment. Ban them, and apply stays pure. Here is the forbidden list and why each item is poison.
| Forbidden | Why it breaks consensus |
|---|---|
| Wall-clock time | now() returns a different value on every node; two nodes executing the same block a second apart would branch. |
| Random numbers | A true RNG is by definition unreproducible; every node would roll different dice and reach different states. |
| Floating point | IEEE-754 rounding varies with compiler, CPU, and flags; 0.1 + 0.2 can differ in the last bit across platforms. |
| Network / filesystem | The result would depend on what some other server or disk returned — an input outside (state, transaction). |
| Threads / concurrency | Scheduling order is nondeterministic; two threads racing on shared data give different results per run. |
| Uninitialised or platform-sized data | Reading undefined memory, or an int whose width depends on the OS, differs across machines. |
Look back at the ethmini VM in rust/ethmini/src/vm.rs and you will see none of these appear. The interpreter loop reads bytecode, pushes and pops a Vec<u64>, and touches a BTreeMap of storage. That is the entire vocabulary. There is no std::time, no rand, no f64, no I/O — and that is not because the author was lazy. It is because every one of those would be a bug that forks the chain.
Where the EVM gets its “randomness” and “time,” then
Section titled “Where the EVM gets its “randomness” and “time,” then”Contracts plainly do need something like time and randomness — an auction has a deadline, a lottery needs a winner. The resolution is that the EVM never generates these; it is handed them as ordinary, consensus-agreed inputs, folded into the state and block data that every node already shares:
- Time comes from the block’s
timestampfield, proposed by the block producer and validated within bounds by everyone. It is data in the block, not a call to a system clock, so every node reads the same number. - “Randomness” on modern Ethereum comes from
PREVRANDAO(post-Merge), a value derived from validators’ beacon-chain contributions. It is again a fixed field every node agrees on, not a fresh dice roll — which is exactly why on-chain “randomness” is subtly manipulable and why serious applications use commit-reveal schemes or external oracles.
The pattern is worth internalising: the EVM turns every would-be source of nondeterminism into a shared input. If everyone reads the same block, everyone computes the same result.
Defined overflow is a determinism requirement
Section titled “Defined overflow is a determinism requirement”Here is the subtlest and most important instance, and it is one we already built. What should u64::MAX + 1 do?
On a normal Rust program in debug mode, that arithmetic panics. In release mode it wraps. On some hypothetical platform it might saturate. Three different behaviours — and that is the problem. If the EVM inherited its host language’s overflow behaviour, the same bytecode would do different things on different builds, and the chain would fork the first time a contract added two large numbers.
So the EVM does not leave overflow to chance. It defines it: arithmetic is modular, wrapping at the word boundary, always, everywhere. Real EVM wraps mod 2²⁵⁶; our teaching model wraps mod 2⁶⁴. Look at how ethmini spells this out in vm.rs:
Op::Add => { let b = pop(&mut stack, "ADD")?; let a = pop(&mut stack, "ADD")?; push(&mut stack, a.wrapping_add(b))?; // defined: wrap, never panic pc = next;}The choice of wrapping_add over plain + is not a micro-optimisation and it is not primarily about safety. It is a consensus rule. A plain + could panic in debug and wrap in release; wrapping_add is defined to do the exact same thing on every target, in every build mode, forever. The test in the crate pins the behaviour down:
#[test]fn arithmetic_wraps_mod_2_64() { // u64::MAX + 1 wraps to 0, just as EVM wraps mod 2^256. let code = assemble(&[Op::Push(u64::MAX), Op::Push(1), Op::Add, Op::Return]); let exec = run(&code, &empty(), 1000).unwrap(); assert_eq!(exec.return_value, Some(0));}The same logic covers every other “what happens at the edge?” case in the machine. Division by zero, an out-of-range jump, a stack that grows too tall — each has a single, specified outcome, so no node ever has to improvise. That is why the ethmini error module enumerates OutOfGas, StackUnderflow, StackOverflow, InvalidOpcode, TruncatedPush, and InvalidJump: every abnormal path is named and given one defined result. There is no “undefined behaviour” corner for two implementations to disagree in.
Canonical serialization: committing state deterministically
Section titled “Canonical serialization: committing state deterministically”Deterministic execution is only half the job. To agree on a state root, nodes must also serialise the resulting state into bytes identically before hashing — because a hash is a function of exact bytes, and any difference in byte order yields a completely different hash.
This is where a very ordinary engineering fact becomes a consensus hazard. ethmini stores accounts in a HashMap, and a HashMap’s iteration order is deliberately randomised per run (a defence against hash-flooding attacks). If we hashed the accounts in iteration order, two nodes — or even the same node on two runs — would serialise the same accounts in different orders and compute different roots. Identical state, different fingerprint: a fork born from nothing but map internals.
The fix is to impose a canonical order before hashing. Here is exactly what state_root in state.rs does:
pub fn state_root(&self) -> [u8; 32] { // Sort accounts by address for a canonical serialisation. let mut sorted: Vec<(&Address, &Account)> = self.accounts.iter().collect(); sorted.sort_by_key(|(addr, _)| **addr); // ... serialise the sorted pairs, then SHA-256 the bytes.}The sort_by_key line is the entire point. Two nodes with the same set of accounts, in whatever random order their maps chose, both sort by address and arrive at the same sequence, serialise to the same bytes, and hash to the same root. Determinism is restored not by making the map ordered, but by making the commitment ignore the map’s order.
node A's HashMap order: 0x7f…, 0x02…, 0x91… ┐ sort by address node B's HashMap order: 0x91…, 0x7f…, 0x02… ┘ │ ▼ both → 0x02…, 0x7f…, 0x91… → same bytes → same state root ✓Under the hood — how real Ethereum makes this structural
Section titled “Under the hood — how real Ethereum makes this structural”Sorting a flat list works for a teaching model, but real Ethereum needs canonical serialization plus efficient proofs plus cheap incremental updates when one account changes. It gets all three from the Merkle-Patricia trie (the state-and-tries part is entirely about this). A trie keys accounts by the bytes of their address, so the structure — and therefore the root hash — is a pure function of the contents, never of insertion order. Encoding is pinned down by RLP (recursive-length prefix), a byte-exact serialization format, so there is exactly one valid byte string for any given state.
The through-line from ethmini’s sort_by_key to Ethereum’s trie is the same idea at two scales: never let a data structure’s internal ordering leak into the hash. ethmini sorts once and re-hashes everything; real Ethereum bakes the canonical order into the structure so it never has to. Both guarantee that the fingerprint depends on what the state is, not how it happened to be stored.
Gas accounting must be deterministic too
Section titled “Gas accounting must be deterministic too”There is a second thing every node must agree on besides the final state: how much gas the transaction burned. The gas used goes into the receipt, the receipts are committed into a receipts root in the block header, and — crucially — whether a transaction runs out of gas changes its outcome (an out-of-gas call reverts). So the gas meter is not bookkeeping bolted on the side; it is part of the consensus rules, and it must produce the identical number on every node.
That is why gas is charged the way it is in ethmini’s run loop: a fixed cost per opcode, deducted before the op acts, from a counter that can never go negative.
let (op, next) = Op::decode(code, pc)?;charge(&mut gas, op.gas_cost())?; // deterministic cost, charged before actingBecause Op::gas_cost is a pure lookup table — Add is always 3, SLoad always 100, SStore always 200 — the same trace of opcodes burns the same gas everywhere. There is no timing, no measured wall-clock CPU cost, no “however long it happened to take.” Metering by a fixed price table rather than by real elapsed time is itself a determinism decision: real time varies per machine and would fork the chain, whereas a table is a shared constant everyone reads the same.
The architect’s lens
Section titled “The architect’s lens”Determinism is not a component you can point at — it is a discipline imposed on every component. But it is a major property, so it earns the lens.
- Why does it exist? Because consensus among untrusting strangers requires that everyone who re-executes a transaction reaches the same answer. Determinism is the property that makes “verify by re-running it yourself” a valid way to agree, instead of trusting whatever a stranger claims the result was.
- What problem does it solve? It removes every hidden input from execution. By forbidding clocks, randomness, floating point, and I/O, and by defining every arithmetic edge case, it guarantees
apply(state, tx)has exactly one possible output — so two honest nodes can never legitimately disagree. - What are the trade-offs? You give up nearly everything a normal runtime offers: no real time, no true randomness, no floats, no concurrency, no network calls from inside execution. Contracts that need those must get them as shared block inputs or push the work off-chain — which is slower and more awkward than just calling a library.
- When should I avoid it? You never avoid determinism inside the EVM — it is mandatory. You avoid putting work in the EVM when that work is inherently nondeterministic (fetching a live price, using real entropy, heavy floating-point math); those belong in oracles, off-chain services, or L2 systems that feed results in as agreed data.
- What breaks if I remove it? Everything. The moment execution can differ between nodes, the state root check fails for honest blocks, nodes reject each other, and the single shared ledger splinters into disagreeing forks. There is no “Ethereum” without determinism — only a crowd of computers each running its own private chain.
The thread
Section titled “The thread”Return to the book’s one question: how do untrusting strangers agree on the state of a shared world computer? This page is the sharpest possible answer. They agree because the world computer is deterministic: anyone, anywhere, can take the same prior state and the same transaction, run them through the exact same rules, and get the exact same next state and the exact same 32-byte root. Agreement is not achieved by trust, or by voting on what the answer should be — it is achieved by making the answer unique, so that re-computing it is the same as verifying it.
Everything the EVM forbids — the clocks, the randomness, the floats, the undefined overflow, the unordered hashing — is forbidden in service of that one guarantee. And the cost half of the question shows up here too: determinism is expensive. It is why on-chain computation is thousands of times dearer than a call to your laptop, why time and randomness are clumsy, and why so much work is pushed to L2s and oracles. You buy the ability of thousands of strangers to agree on one ledger by giving up almost everything a normal computer takes for granted. That is the bargain the EVM makes on every single instruction.
Next, tie the whole part together: Revision · The EVM →
Check your understanding
Section titled “Check your understanding”- Write the signature of the state-transition function as a pure function, and explain what “pure” means here. What inputs is
applynot allowed to read, and what goes wrong if it does? - A node receives a block whose header claims
stateRoot = 0x9f3c…. Describe exactly what the node does to accept or reject it, and why that check is only meaningful because of determinism. ethminiusesa.wrapping_add(b)instead ofa + b. Explain why this is a consensus decision and not merely a safety or performance one. What could go wrong with plain+?WorldState::state_rootcallssort_by_keybefore hashing the accounts. What specific nondeterminism does that line defend against, and how does real Ethereum get the same guarantee structurally?- Gas used is committed into the block and must match on every node. Why does the EVM meter by a fixed per-opcode price table rather than by measured CPU time, and what would break if it used elapsed time instead?
Show answers
apply : (State, Transaction) → (State', Receipt). Pure means the output depends only on the two declared inputs — the same(state, tx)always yields the same(state', receipt)on every machine.applymay not read anything outside those inputs: no wall-clock time, no random numbers, no network or filesystem, no thread scheduling, no platform-dependent behaviour. If it reads any of those, two honest nodes can compute different next states, produce different state roots, reject each other’s blocks, and the chain forks.- The node re-executes every transaction in the block itself, computes its own resulting state root, and compares it byte-for-byte to the
0x9f3c…in the header — accept if equal, reject if not. This is only meaningful because execution is deterministic: if the same transactions could produce different roots on different machines, an honest node would compute a different root and wrongly reject an honest block. Determinism is what makes “I got the same root” equivalent to “the block is valid.” - Plain
+in Rust has build-dependent behaviour — it panics on overflow in debug builds and wraps in release builds — so the same bytecode could do different things on different nodes, forking the chain the first time a contract overflowed.wrapping_addis defined to wrap at the word boundary on every target and every build mode, giving one specified result everywhere. Overflow behaviour is therefore a consensus rule: it must be identical for all nodes, which is why it is pinned rather than inherited from the host language. ethministores accounts in aHashMap, whose iteration order is randomised per run; hashing the accounts in that order would give the same state two different roots on two nodes (or two runs).sort_by_keyimposes a canonical order (by address) so the serialised bytes — and thus the root — depend only on the contents of the state, not on map internals. Real Ethereum gets this structurally from the Merkle-Patricia trie plus RLP encoding: the trie keys by address so its root is a pure function of contents, and RLP pins down exactly one valid byte string per state.- A fixed price table (
Add= 3,SLoad= 100,SStore= 200, …) is a shared constant every node reads identically, so the same opcode trace burns the same gas everywhere — and gas used, plus whether a call ran out of gas, is part of consensus. Measured CPU time varies per machine (faster hardware, load, OS scheduling), so metering by elapsed time would make nodes disagree on gas used and on whether a transaction reverted, forking the chain. Metering by table rather than clock is itself a determinism requirement.