JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

From a public key to an address: hashing, encoding, and checksums

An address is a fingerprint of your key, not the key itself. Derive one end to end — hash, truncate, encode, checksum — and see why Bitcoin and Ethereum end up looking so different.

An address is a fingerprint, not the key

When you ask a friend to pay you, you send them a short string like `1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa` or `0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed`. That string is your address. It is not your public key, and it is certainly not your private key — it is one more one-way door past both of them. By now you have built the whole chain in this rung: a secret number (private key) generates a public key on the curve, you can sign with the secret and verify with the public. An address is the last link: take the public key and run it through a cryptographic hash. The output is a fingerprint — short, public, and impossible to run backwards.

So the full pipeline is three one-way steps stacked on top of each other: private key → public key → address. Multiplying by the curve generator hides the private key behind the public key. Hashing hides the public key behind the address. You can publish your address on a business card and nobody can climb back up the ladder to your money.

Bitcoin: HASH160 and Base58Check

Let's derive a classic Bitcoin address (a legacy pay-to-public-key-hash, or P2PKH, address — the kind that starts with `1`). Bitcoin doesn't hash once; it hashes twice with two different functions, then wraps the result with a version byte and a checksum, and finally encodes the whole thing in Base58.

  1. Start with the public key: `04 || X || Y` (65 bytes uncompressed) or `02/03 || X` (33 bytes compressed). The two forms produce different addresses from the same key — a real-world gotcha.
  2. Compute HASH160 = `RIPEMD160(SHA256(pubkey))`. The SHA-256 gives 32 bytes; RIPEMD-160 squeezes that to a 20-byte public-key hash.
  3. Prepend the version byte `0x00` (mainnet P2PKH). This is what forces the final address to start with `1`.
  4. Compute the checksum: `SHA256(SHA256(version || hash160))`, and keep only the first 4 bytes.
  5. Concatenate `version || hash160 || checksum` (25 bytes total) and Base58-encode it. Done.
# Bitcoin legacy (P2PKH) address, from a public key
pubkey   = 04 || X || Y                    # 65-byte uncompressed point on secp256k1
                                           # (or 33-byte compressed: 02/03 || X)
h160     = RIPEMD160(SHA256(pubkey))       # 20-byte "HASH160"
payload  = 0x00 || h160                    # 0x00 = mainnet P2PKH version byte (-> '1')
checksum = SHA256(SHA256(payload))[0:4]    # first 4 bytes = 32-bit checksum
address  = Base58( payload || checksum )   # 25 bytes -> a string starting with '1'

# Real example: the very first coinbase output (Bitcoin genesis block) pays to
#   1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
P2PKH derivation. Hashing twice with SHA-256 then RIPEMD-160 is Bitcoin's HASH160.

Ethereum: Keccak-256 and the last 20 bytes

Ethereum takes a blunter path. There is no version byte, no Base58, no double hash. You hash the public key once with Keccak-256 and simply keep the last 20 bytes. Then you write those 20 bytes as hexadecimal with a `0x` prefix — 40 hex characters, and that's the address.

# Ethereum address, from a public key
pubkey  = X || Y                       # 64 bytes: the raw curve point, NO 0x04 prefix
digest  = Keccak256(pubkey)            # 32-byte hash
address = "0x" + hex( digest[12:32] )  # keep the LAST 20 bytes -> 40 hex chars

# e.g.  0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed  (all-lowercase, no checksum yet)
Ethereum drops the first 12 bytes of the Keccak-256 digest and keeps the last 20.

Why is truncating to 20 bytes safe? Because the hash's collision resistance and preimage resistance still hold over the truncated output — finding two public keys that collide on the same 160-bit address is a ~2^80 birthday-bound effort, far beyond reach today. The 12 discarded bytes simply trade a sliver of theoretical security for a shorter, friendlier address, exactly as Bitcoin's RIPEMD-160 does.

Checksums: catching the fat-finger before it costs you

Here is the brutal fact a checksum exists to guard against: a blockchain payment is irreversible. There is no bank to call. If you mistype one character and the result happens to be a valid-looking address, your coins are gone. A checksum is a few extra bits baked into the address so that most typos produce a string that fails the check and your wallet refuses to send.

Bitcoin's checksum is strong and explicit. Those 4 bytes from the double-SHA-256 are a full 32-bit checksum: the chance that a random corruption still passes is about 1 in 2^32 ≈ 1 in 4.3 billion. Change any character and Base58Check almost certainly rejects it.

Ethereum's checksum is sneakier — it hides in the letter casing. The raw address is case-insensitive hex, so EIP-55 reuses the upper/lower case of the letters `a–f` to carry a checksum, while keeping the address the same 40 characters. You hash the lowercase address, and for each hex letter you uppercase it when the matching bit of the hash is set:

# EIP-55: a checksum smuggled into the upper/lower casing of an Ethereum address
def to_checksum(addr40):                 # addr40 = lowercase hex, no "0x"
    h = Keccak256(ascii(addr40))         # hash the lowercase hex STRING (as text)
    out = ""
    for i in range(40):
        c = addr40[i]
        if c in "0123456789":
            out += c                     # digits carry no checksum, never change
        elif nibble(h, i) >= 8:          # is the i-th hex digit of the hash >= 8?
            out += upper(c)              #   yes -> UPPERCASE this letter
        else:
            out += lower(c)              #   no  -> lowercase this letter
    return "0x" + out

# 0x5aaeb6...  ->  0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
EIP-55. The mixed case IS the checksum; a wallet recomputes it to spot a typo.

Why an address is not the key — and what that buys you

Because an address is a hash, the arrow only points one way. You cannot go from an address back to the public key, and you certainly cannot reach the private key — that would mean inverting both the hash and the elliptic-curve multiplication. This is the bedrock of self-custody: you can hand your address to the entire world and reveal nothing that lets anyone spend your funds.

There's a subtle bonus in Bitcoin. Since the address is a hash of the public key, the public key itself stays hidden until the first time you spend from that address (spending reveals the key so the network can verify your signature). For an address you've never spent from, an attacker sees only the hash — a small extra cushion sometimes cited in the context of a future quantum attacker who could break the curve but not the hash.

Two more honest caveats. First, privacy: because the ledger is public, reusing one address links all your activity together — wallets generate a fresh address per payment for a reason. Second, finality of mistakes: in a self-custodial wallet you alone hold the key behind the address. Lose the key and the funds at that address are unrecoverable; send to the wrong (but valid) address and nobody can claw it back. A checksum proves an address is well-formed. It does not prove you own it, and it does not prove it belongs to the right person.

Beyond public keys: contract addresses and one seed, many addresses

Not every address is the fingerprint of a public key. On Ethereum, a smart contract has an address too — and it's a hash of deployment data, not of any key. A plain `CREATE` hashes the deployer plus its transaction count; the CREATE2 opcode hashes the deployer, a chosen salt, and the contract's own init code, which lets you compute a contract's address before you deploy it.

# Ethereum contract addresses are ALSO hashes -- of deployment data, not a key
CREATE  : addr = keccak256( rlp_encode([sender, nonce]) )[12:32]
CREATE2 : addr = keccak256( 0xff || sender || salt || keccak256(init_code) )[12:32]
#          ^ same 'last 20 bytes of a Keccak-256 hash' rule as an account address
Contract addresses follow the same 'last 20 bytes of Keccak-256' shape.

And in the other direction, one secret can mint thousands of addresses. A modern wallet is hierarchical-deterministic (BIP-32): from a single seed phrase it derives a tree of key pairs, and BIP-44 standardizes the derivation paths so the same twelve words regenerate the same addresses in any wallet app. That is why your wallet can show a fresh receiving address every time — better for privacy — while you still only back up one seed.