Skip to content

Keccak-256: Ethereum's Hash (and Why It Isn't SHA-3)

The previous page, secp256k1: Keys as Points on a Curve, gave us a way to own something: a private scalar nobody else can guess, and a public point anyone can check a signature against. But a 65-byte public point is not an address, a storage location is not a raw key, and “call the transfer function” is not a string the EVM understands. Something has to turn arbitrary bytes into short, fixed-size, collision-proof labels. That something is a hash function, and in Ethereum it is almost always Keccak-256.

This page builds the one primitive that shows up in every corner of the protocol. When we compute an address on the next page, match a transaction to a function, or index an event log, we are calling Keccak-256. And there is a famous trap waiting in the family name: the hash Ethereum uses is not the NIST standard SHA3-256, even though they come from the same design. Getting that wrong is the single most common cryptographic mistake a newcomer makes here, and this page exists mostly to make sure you never make it.

Recall the throughline of this whole book: untrusting strangers must agree on the state of a shared world computer. Agreement means everyone can independently recompute the same short fingerprint of the same data and get the same answer, byte-for-byte, forever. A cryptographic hash is the tool that makes a fingerprint trustworthy. For it to be usable as the backbone of addresses and commitments, it must give us four properties.

  • Deterministic. The same input always produces the same 32-byte output, on every machine, in every client, for all time. If two nodes hashed the same account differently, they could never agree on state. (This is exactly why the companion crate sorts its accounts before hashing — see the note below.)
  • Preimage-resistant. Given a digest h, you cannot feasibly find any input x with hash(x) = h. This is what lets an address — a hash of your public key — be published safely: it reveals nothing usable about the key behind it.
  • Collision-resistant. You cannot feasibly find two different inputs x != y with hash(x) = hash(y). If you could, you could forge a commitment: promise one thing, reveal another with the same hash.
  • Avalanche effect. Flip a single input bit and roughly half the output bits flip, unpredictably. There is no locality — you cannot nudge a digest toward a target by nudging the input.

These are the same guarantees we lean on everywhere: for addresses (preimage resistance protects the key), for storage-slot keys (determinism makes lookups reproducible), for commitments and Merkle roots (collision resistance makes a promise binding). Keccak-256 is chosen because, as far as public cryptanalysis knows, it delivers all four at a 256-bit output width.

Older hashes like SHA-256 are built from a Merkle–Damgård chain: absorb the message block by block into a fixed-size internal state. Keccak uses a different design called a sponge, and understanding the shape of it is what makes the SHA-3 confusion later make sense.

A sponge keeps a large internal state of b bits, split into two regions: the rate r (the part that touches the message) and the capacity c (a hidden reserve that is never directly read or written by the message, and is what buys the security margin). For Keccak-256, b = 1600, r = 1088, c = 512.

message (after padding, chopped into r-bit blocks)
│ │ │
▼ ▼ ▼
┌─────────────────────────────┐
│ ABSORB: for each block, │
│ XOR it into the rate, then │ f = Keccak-f[1600] permutation
│ run the permutation f │ (24 rounds of mixing)
└─────────────────────────────┘
▼ (message exhausted)
┌─────────────────────────────┐
│ SQUEEZE: read r bits out, │
│ run f, repeat until you │──► take the first 256 bits = digest
│ have enough output │
└─────────────────────────────┘
state = [ rate r = 1088 bits | capacity c = 512 bits ]
▲ ▲
message touches this never touched directly

Two phases: absorb the message by XOR-ing each block into the rate and stirring with the Keccak-f[1600] permutation, then squeeze output out of the rate the same way. For a 256-bit digest one squeeze is enough — you just read the first 256 bits of the state. The capacity c is the security parameter: it is why the collision resistance is 2^(c/2) = 2^256… wait, c = 512, so 2^256? The published security claim tracks the output width, giving 2^128 collision resistance for a 256-bit digest, which matches the birthday bound above. The capacity is generous precisely so the output width, not the internals, is the binding constraint.

Under the hood — padding, and where SHA-3 forks off

Section titled “Under the hood — padding, and where SHA-3 forks off”

Before absorbing, the message must be extended to a whole number of r-bit blocks. Every sponge needs a padding rule. Keccak’s rule appends a 1 bit, then enough 0 bits, then a final 1 bit (the 10*1 “multi-rate padding”). At the byte level, for a message that ends on a byte boundary, the first padding byte is where the story splits:

original Keccak (what Ethereum uses):
append domain byte 0x01, then pad with 0x00…, final byte carries 0x80
NIST SHA-3 (FIPS 202, standardized 2015):
append domain byte 0x06, then pad with 0x00…, final byte carries 0x80

That is the entire cryptographic difference between Keccak-256 and SHA3-256: a single domain-separation byte in the padding — 0x01 versus 0x06. The permutation is identical, the rate and capacity are identical, the round count is identical. NIST added the extra 0x06-vs-0x01 bits as a “domain separator” during standardization, partly so SHA-3 output could be distinguished from other sponge-based functions (like the SHAKE extendable-output functions). It was a late, deliberate change — and Ethereum had already shipped the un-separated original.

The practical consequence is brutal in its simplicity: the same input, hashed with “Keccak-256” and with “SHA3-256”, gives two completely different 32-byte digests. Because of the avalanche effect, they share nothing.

Keccak-256 vs SHA3-256: the trap, concretely

Section titled “Keccak-256 vs SHA3-256: the trap, concretely”

The cleanest way to internalize this is the empty-string test vector, which every library ships and which you can memorize:

Keccak-256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
SHA3-256("") = a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a

Two different constants for the empty input. If your tooling returns a7ffc6… when you asked for a keccak256 hash, it is silently computing the wrong thing — and everything downstream (your address, your selector, your storage slot) will be wrong.

The naming disaster is what makes this dangerous:

  • Solidity’s global function is spelled keccak256(...). It computes original Keccak — correct for Ethereum.
  • Many language libraries expose a module or class literally named sha3 or SHA3 that, for historical reasons, computes original Keccak (they predate FIPS 202) — also correct, but for the wrong reason, and confusingly named.
  • Other libraries updated sha3 to mean the standardized SHA3-256 — now incorrect for Ethereum, from a call spelled identically to the one that used to work.

Once addresses, contract addresses, storage layouts, and function selectors were all defined in terms of original Keccak, the hash became consensus-critical: every full node must compute exactly the same digests to agree on the same state. Migrating to SHA3-256 later would have changed every address and every storage slot on the chain — a hard fork of the entire meaning of the ledger, for zero security benefit (the two are equally strong; only the padding byte differs). So Ethereum kept the original. The “wrong” hash, frozen at the wrong moment for entirely accidental reasons, is now permanent and correct-by-definition for this chain. It is a small, honest monument to a real engineering truth: a shipped protocol’s choices harden into consensus, and consensus is far more expensive to change than to have gotten “right” up front.

The same 32-byte primitive does four distinct jobs. Each one is just “hash these bytes and keep some of the output.”

1. Address derivation. A user account’s address is the last 20 bytes of the Keccak-256 hash of the (uncompressed, prefix-stripped) public key. Contract addresses are keccak256(rlp(sender, nonce))[12:] (or, for CREATE2, a hash of the deployer, a salt, and the init-code). We do this in full on the next page.

2. Storage-slot keys. The EVM’s storage is a flat map from 256-bit key to 256-bit value. A plain variable lives at a fixed slot, but the slot for mapping[k] is keccak256(k . slot_of_mapping), and a dynamic array’s elements start at keccak256(slot). Hashing is what spreads a mapping’s entries across the huge 256-bit slot space so they never collide.

3. The 4-byte function selector. When you call transfer(address,uint256), the EVM has no names — it dispatches on the first four bytes of keccak256("transfer(address,uint256)"). That canonical-signature string (no spaces, no argument names, just types) is hashed, and the leading four bytes select the function.

// The selector is the first 4 bytes of the Keccak-256 of the signature string.
// bytes4(keccak256("transfer(address,uint256)")) == 0xa9059cbb
bytes4 selector = bytes4(keccak256(bytes("transfer(address,uint256)")));

4. Event topic hashing. A non-anonymous event’s first log topic is keccak256 of its canonical signature, e.g. keccak256("Transfer(address,address,uint256)"). That is how off-chain indexers filter the log stream for exactly the events they care about.

Notice the pattern: in every case Keccak-256 turns a variable, human-meaningful thing (a key, a mapping index, a function name, an event shape) into a fixed-size, collision-proof label the protocol can index by. That is the whole job.

// The shape used across Ethereum tooling. `Keccak256` here is the ORIGINAL
// Keccak (0x01 padding), NOT SHA3-256 — the `sha3` crate exposes both, and
// picking the wrong type silently computes the wrong digest.
use sha3::{Digest, Keccak256};
fn keccak256(bytes: &[u8]) -> [u8; 32] {
let mut hasher = Keccak256::new();
hasher.update(bytes);
hasher.finalize().into()
}
// Address = last 20 bytes of keccak256(public_key_x || public_key_y).
fn address_from_pubkey(pubkey_64: &[u8; 64]) -> [u8; 20] {
let digest = keccak256(pubkey_64);
let mut addr = [0u8; 20];
addr.copy_from_slice(&digest[12..32]); // keep the LAST 20 bytes
addr
}
  • Why does it exist? Ethereum needs one deterministic, collision-proof way to turn arbitrary bytes into short fixed-size labels that every node computes identically. Keccak-256 is that primitive, chosen in 2014–2015 when Keccak had just won the SHA-3 competition and looked like the future.
  • What problem does it solve? It lets untrusting strangers agree: addresses derived from keys, storage slots derived from mapping keys, functions dispatched by name, events indexed by signature — all reproducible from public rules with no trusted party.
  • What are the trade-offs? Its only real trade-off is a naming one: it is original Keccak, not standardized SHA3-256, so libraries labeled sha3 may compute the wrong digest. Cryptographically the two are equally strong; the cost is purely the risk of picking the wrong variant.
  • When should I avoid it? Never, for Ethereum-consensus work — using anything else breaks compatibility. Outside Ethereum, if you specifically need FIPS/NIST compliance, use SHA3-256 (or SHA-256), because original Keccak is not the certified standard.
  • What breaks if I remove it? Everything. Addresses become underivable, storage layouts undefined, function calls undispatchable, event logs unfilterable. Keccak-256 is load-bearing under the entire account model and the ABI.
  1. In one sentence, what is the only cryptographic difference between Keccak-256 and NIST SHA3-256, and what visible consequence does it have?
  2. Name the four properties a hash must have to back Ethereum’s addresses and commitments, and give one place each property is relied on.
  3. A library exposes a function called sha3_256. Why is that name not enough to tell you whether it is safe to use for computing an Ethereum address, and what one-line test settles it?
  4. List the four distinct jobs Keccak-256 does in Ethereum, and for each, say what bytes go in and what part of the digest is kept.
  5. Using the book’s throughline, explain why Ethereum keeping the “wrong” (un-standardized) hash is now the correct choice and cannot be changed.
Show answers
  1. They differ only in one domain-separation padding byte (original Keccak appends 0x01, SHA3-256 appends 0x06); because of the avalanche effect this makes their digests for the same input completely different — e.g. Keccak256("") = c5d2460… vs SHA3-256("") = a7ffc6f8….
  2. Deterministic (every node must recompute the same state root / digest to agree); preimage-resistant (an address can be published without leaking the public key behind it); collision-resistant (a commitment or Merkle root is binding — you can’t find a second input with the same hash); avalanche effect (you can’t steer a digest toward a target by tweaking the input). Any one relied-on site per property is fine.
  3. Because sha3/SHA3 is historically ambiguous: some libraries named that way compute original Keccak (correct for Ethereum), others compute standardized SHA3-256 (wrong for Ethereum). The one-line test: hash the empty string — if you get c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 it is Keccak (correct); if you get a7ffc6f8… it is SHA3-256 (wrong).
  4. (a) Address derivation — public key bytes in, keep the last 20 bytes. (b) Storage-slot keys — mapping key concatenated with the mapping’s slot in, keep the full 32 bytes as the slot. (c) Function selector — the canonical signature string like "transfer(address,uint256)" in, keep the first 4 bytes. (d) Event topic — the canonical event signature in, keep the full 32 bytes as topic 0.
  5. Once addresses, storage slots, selectors, and event topics were all defined in terms of original Keccak, that hash became consensus-critical: every node must compute identical digests to agree on the shared world state. Migrating to SHA3-256 would change every address and slot on the chain — a total redefinition of the ledger — for zero security gain, so the accidental early choice is frozen, and “correct for this chain” now just means what those rules produce.