Skip to content

From Public Key to a 20-Byte Address

You now have two ingredients from the last two pages. From secp256k1: Keys as Points on a Curve you have a public key: a point on the curve, written as two 256-bit coordinates (x, y). From Keccak-256: Ethereum’s Hash you have the exact hash function Ethereum uses everywhere. This page combines them into the one artifact you actually paste into a wallet: an address.

An Ethereum address is 20 raw bytes. That is the whole thing — no version byte, no network prefix, no Base58 or Bech32 layer. This page gives you the exact recipe, works a concrete example so you can reproduce every intermediate value yourself, and then adds the one piece of cleverness that lives on top: EIP-55, a mixed-case checksum that catches typos without changing the address by a single bit. The point of the throughline here is small but sharp: for untrusting strangers to move value, they must first agree on who an account is — and “who” reduces to 20 deterministic bytes anyone can recompute from a public key.

Given a private key, the derivation is fully mechanical. Every node, wallet, and library does exactly this:

1. private key d (a 256-bit scalar)
│ elliptic-curve multiply: P = d · G
2. public key P = (x, y) (two 32-byte coordinates)
│ serialize UNCOMPRESSED, then DROP the 0x04 prefix byte
3. 64 bytes: x ‖ y (32 bytes of x, then 32 bytes of y)
│ Keccak-256
4. 32-byte hash (256 bits)
│ keep the LAST 20 bytes; discard the leading 12
address (20 bytes, shown as 0x-prefixed hex)

Two details in that pipeline trip people up, so state them plainly:

  • Drop the 0x04. An uncompressed secp256k1 public key is 65 bytes: a 0x04 tag byte followed by the 32-byte x and the 32-byte y. Ethereum hashes only the 64-byte coordinate pair x ‖ y. The 0x04 is a serialization tag, not part of the key’s value, so it is stripped before hashing. Hash the 65-byte form by mistake and you get a completely different, wrong address.
  • Keccak-256, not SHA3-256. As the previous page laboured, Ethereum uses the original Keccak padding, not the NIST SHA3-256 that shipped later. They give different digests for the same input. Every address in existence depends on this being Keccak.

The address is the rightmost 20 bytes of the 32-byte hash — bytes 12 through 31, zero-indexed. The leading 12 bytes are simply thrown away.

Two independent design questions hide here.

Why 20 bytes at all? A 160-bit address gives roughly 2^160 possible values. That is enough that finding a private key whose address collides with a specific target (a second-preimage attack) is computationally hopeless — around 2^160 work. It is also compact: 20 bytes is 40 hex characters, short enough to be human-manageable while astronomically large. Bitcoin’s legacy addresses landed on the same 160-bit width for the same reason. The trade-off is honest: 160 bits gives less collision margin than the full 256-bit hash would, but a general collision (two keys sharing any address) still costs about 2^80 work by the birthday bound — far beyond reach, and irrelevant to stealing a chosen account.

Why the last 20 and not the first? For Keccak, this genuinely does not matter — every output bit is equally well mixed, so any fixed 20-byte window is as good as any other. Taking the trailing bytes is a convention, chosen once and now frozen into consensus. There is no security reason to prefer the tail; there is every reason for all software to agree on the same window, and “the last 20” is what everyone agreed on.

Numbers make this checkable. Take a deliberately trivial private key so you can reproduce every step with any Keccak library:

private key d = 0x0000...0001 (the scalar 1)

Multiplying the generator point G by 1 gives G itself, so the public key is exactly the generator’s coordinates (these are fixed constants of secp256k1):

x = 79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798
y = 483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8

Concatenate x ‖ y into 64 bytes (this is the public key without the 0x04 tag), run Keccak-256 over those 64 bytes, and you get a 32-byte digest. Its last 20 bytes are the address:

keccak256(x ‖ y) = ...(12 discarded bytes)... 7e5f4552091a69125d5dfcb7b8c2659029395bdf
└──────────┬────────────┘ └──────────────────┬───────────────────┘
dropped (12 bytes) address (20 bytes)
address = 0x7e5f4552091a69125d5dfcb7b8c2659029395bdf

You can verify this with any tool that exposes Keccak-256. The point is not the specific value; it is that the derivation is deterministic and public. Given the same public key, every participant on Earth computes the same 20 bytes, with no secret and no coordination — which is exactly why strangers can agree on account identity without trusting each other or any authority.

EIP-55: a checksum that hides in the letter case

Section titled “EIP-55: a checksum that hides in the letter case”

The raw address above is all-lowercase. But typos happen — a wallet UI, a copied string, a hand-transcribed address — and a mistyped address is not rejected by the protocol. Ethereum has no per-address checksum baked into the format the way Bitcoin’s Base58Check does. Send funds to a wrong-but-well-formed 20-byte address and they are gone: some contract or empty account you didn’t mean, unrecoverable.

EIP-55 (Vitalik Buterin, 2016) solves this without touching the protocol at all. The trick: hex digits af can be written upper- or lowercase, and case carries no meaning for the address itself. So EIP-55 encodes a checksum into the case pattern. A wallet that understands EIP-55 can then reject an address whose case pattern is inconsistent — catching typos — while a wallet that ignores case still treats it as the same address. Zero consensus change; it is purely a display-and-validation convention.

1. Take the address as 40 lowercase hex characters (no "0x").
2. Compute h = keccak256( the 40 ASCII lowercase hex chars ).
(Note: hash the HEX TEXT, not the 20 raw bytes.)
3. For each of the 40 hex characters, position i:
- if the character is a digit 0-9, leave it as-is.
- if it is a letter a-f, look at hex nibble i of h:
nibble >= 8 → uppercase the letter
nibble < 8 → lowercase the letter
4. Prepend "0x".

Each hex character of the address is paired with one hex nibble of the hash, and the top bit of that nibble decides the case. Roughly half the letters get uppercased, giving a distinctive mixed-case look:

lowercase: 0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed
EIP-55: 0x5aAeb6053F3e94C9b9A09f33669435E7Ef1BeAed

That second form is the canonical EIP-55 rendering of the same address. Notice the address value is byte-for-byte identical — only the case of the af letters changed. A wallet validates by recomputing step 2–3 and checking that the case pattern matches; a mismatch means at least one character is wrong.

Under the hood — how much protection it actually gives

Section titled “Under the hood — how much protection it actually gives”

The checksum is stronger than it looks. For an address with k letters (af) among its 40 characters, EIP-55 fixes the case of each of those k letters to one specific value out of two. A random single-character typo that lands on a letter, or a case that got mangled, has to happen to match the hash-derived case to slip through. The EIP’s own analysis puts the odds of a random error going undetected at well under 0.1% for a typical address (with k around 15–20 letters, roughly 2^-k for case-flip errors). It is not a cryptographic guarantee — it is a cheap, high-value guardrail that turns most fat-finger mistakes into an instant client-side rejection instead of a permanent loss.

Two honest limits. First, EIP-55 only helps if the receiving software validates it — an old wallet that ignores case gives you no protection. Second, it protects against typos, not against malice: an attacker who generates a vanity address, or who swaps your clipboard for a different valid checksummed address (clipboard-hijacking malware is real), produces a string that passes EIP-55 perfectly. The checksum defends against slips of the finger, not against a substituted address.

Contrast: an Ethereum address vs. a Bitcoin address

Section titled “Contrast: an Ethereum address vs. a Bitcoin address”

If you came through the Bitcoin material, the difference is stark, and it is worth naming precisely because it explains why Ethereum addresses feel so bare.

Bitcoin (legacy / Bech32)Ethereum
Hash of the pubkeyRIPEMD160(SHA256(pubkey)) → 20 bytesKeccak256(pubkey) → keep last 20 bytes
Version / network byteyes (e.g. 0x00 mainnet P2PKH)none
EncodingBase58Check or Bech32 (built-in checksum)raw hex, 0x-prefixed
Checksummandatory, in the encodingoptional, via EIP-55 letter case
What you actually see1A1zP1... / bc1q...0x7e5f45...

An Ethereum address is literally 20 raw bytes shown as hexadecimal. There is no version byte telling you the network, no Base58Check envelope carrying a checksum, no Bech32 human-readable prefix. Where Bitcoin bakes error-detection and network-tagging into the address format, Ethereum keeps the address as bare bytes and bolts the checksum on afterward as a case convention. The design philosophies differ: Bitcoin’s address is an encoded, self-describing object; Ethereum’s address is the identity itself, and everything else (network, checksum) lives outside it.

One important caveat, and a bridge to later parts. The recipe on this page derives the address of an externally owned account (EOA) — an account controlled by a private key. But Ethereum has a second kind of account: a contract, and its address is not derived from any public key, because a contract has no key.

A contract’s address is computed from the creator’s address and the creator’s nonce at deployment time (roughly keccak256(rlp_encode(creator_address, nonce)), last 20 bytes) — or, with the CREATE2 opcode, from the creator, a salt, and the code’s hash. Either way the shape is the same 20 bytes in the same hex, indistinguishable at a glance from an EOA. The companion ethmini crate makes this dual origin explicit: its Address is a plain [u8; 20], and contract addresses are minted from the creator and nonce rather than from a key.

// ethmini models an address as bare 20 bytes — the shape is real,
// regardless of whether the address came from a key or from a deploy.
pub struct Address(pub [u8; 20]);

The upshot: not every 20-byte address on Ethereum traces back to a public key. Some are the fingerprint of a public key (EOAs); some are the fingerprint of a deployment (contracts). You cannot tell which from the address alone — you have to look at whether the account has code. We come back to contract-address derivation in full when we build accounts and deployment in later parts; for now, hold the two origins side by side.

  1. Write out the four steps that turn a private key into an Ethereum address, being explicit about how many bytes enter the Keccak-256 step and which bytes of the digest become the address.
  2. Why is the 0x04 prefix of an uncompressed public key dropped before hashing, and what goes wrong if you forget to drop it?
  3. EIP-55 does not change the address at all — so what does it change, and how does a wallet use that to catch a typo? What input do you actually feed to Keccak-256 for the checksum?
  4. Give two concrete ways an Ethereum address differs from a Bitcoin address in format, and explain why Ethereum needs EIP-55 at all given Bitcoin’s addresses have a built-in checksum.
  5. Your friend says “every Ethereum address is the hash of a public key.” Correct them, and explain how a contract address is derived instead.
Show answers
  1. (a) Multiply the generator G by the private key d to get the public key P = (x, y). (b) Serialize P uncompressed and drop the 0x04 tag, leaving 64 bytes: x ‖ y. (c) Keccak-256 those 64 bytes to get a 32-byte digest. (d) Keep the last 20 bytes (bytes 12–31), discarding the leading 12; that is the address, shown as 0x + 40 hex.
  2. The 0x04 is only a serialization tag marking the key as uncompressed; it is not part of the key’s value. Ethereum defines the address over the 64-byte coordinate pair, so the tag is stripped first. If you leave it in, you hash 65 bytes instead of 64, get a completely different digest, and derive a wrong (and unusable) address.
  3. It changes the case of the af letters in the hex. The case pattern encodes a checksum: hash the 40 lowercase hex characters (the ASCII text, not the 20 raw bytes) with Keccak-256, then uppercase each letter whose corresponding hash nibble is >= 8. A wallet recomputes this and rejects the address if the case pattern doesn’t match, catching most typos. You feed Keccak-256 the 40-character lowercase hex string.
  4. Any two of: Ethereum has no version/network byte (Bitcoin does); Ethereum uses raw hex while Bitcoin uses Base58Check/Bech32; Ethereum’s checksum is optional and lives in letter case while Bitcoin’s is mandatory and built into the encoding. Ethereum needs EIP-55 precisely because its address format has no built-in checksum — a mistyped but well-formed 20-byte address would otherwise be silently accepted, so the case-based checksum retrofits the error detection Bitcoin gets from its encoding.
  5. Only externally owned accounts derive their address from a public key. A contract has no key; its address is computed from the creator’s address and nonce (keccak256(rlp(creator, nonce)), last 20 bytes), or from creator + salt + code hash under CREATE2. Both kinds are the same 20-byte hex shape, so you can’t tell them apart without checking whether the account has code.