DEV Community

MEROLINE LIZLENT
MEROLINE LIZLENT

Posted on

Bitcoin Keys and Addresses

Have you ever looked at one of these Bitcoin addresses, such as bc1q... or 1A1zP1... and wondered what it meant, or what was happening? We will go over how a private key becomes a public key, and how that public key becomes the address you use to send the money.

You can't access this feature without having a private key.

A Bitcoin private key is not magical; rather, it is a 256-bit random number. That's it. 32 bytes, 64 hex characters, selected preferably from an cryptographically-secure random number generator.

The Elliptic Curve Digital Signature Algorithm (ECDSA) is used on a curve that is known as secp256k1. Nearly any number of 256 bits is a valid private key under the parameters of this curve.

The downside: the security of your money is dependent on the randomness of that number. If your key is based on something that you can guess, such as your birthday, a memorable phrase, a weak PRNG, etc., then there are programs that can easily discover the key in mere seconds and your coins are lost. Bitcoin doesn't offer you a "forgot password" link, there's no second chance.

With ECDSA, it is possible to derive the public key from the private key, which means that the address is derived from the public key and that the possession of the private key is sufficient to get the address.

Displaying a Private key: WIF and WIF-compressed

A raw 32-byte number isn't fun to copy around, so Wallets show private keys as Wallet Import Format (WIF). WIF is simply Base58Check encoding (Base58 + version byte + 4-byte checksum) of the key bytes.

The compressed variant, WIF-C, puts an additional 0x01 byte in front of the encoding to indicate the public key is to be compressed (more on that below). Most modern wallets default to WIF-C as that is what helps maintain a smaller blockchain.

Mainnet prefix Testnet prefix
WIF starts with 5 starts with 9
WIF-Compressed starts with K or L starts with c

Using a Python library such as bitcoinutils, generating both forms from the same secret exponent looks roughly like this:

from bitcoinutils.setup import setup
from bitcoinutils.keys import PrivateKey

setup('testnet')

priv = PrivateKey(secret_exponent=0x1a2b3c...)  # your 256-bit int
print(priv.to_wif(compressed=False))
print(priv.to_wif(compressed=True))
Enter fullscreen mode Exit fullscreen mode

From private key to public key

Elliptic curve math allows you to easily convert from private to public, but not public to private (which would involve brute forcing the curve, a task that's too hard to do in the real world). The public key is obtained by multiplying the private key by a fixed point on the curve, G (the generator point):

P = k * G
Enter fullscreen mode Exit fullscreen mode

P is a point on the curve with coordinates (x, y), each 32 bytes. Naively, that's 64 bytes for a public key, plus a 0x04 prefix to mark it as "uncompressed", 65 bytes total.

Because the curve is symmetric about the x-axis, for any given x there are only two possible y values (even or odd). So instead of storing the full y, we can store just a single bit indicating which one it is. That gives us the compressed public key: 33 bytes, prefixed with 0x02 (even y) or 0x03 (odd y).

pub = priv.get_public_key()
print(pub.to_hex())                  # compressed, 33 bytes
print(pub.to_hex(compressed=False))  # uncompressed, 65 bytes
Enter fullscreen mode Exit fullscreen mode

Compressed keys are the default these days they're shorter, and Segregated Witness actually requires them.

From public key to address

One of the things that comes as a surprise to some new to Bitcoin is that you do not have to share your public key in order to receive funds. Address is a hash of the public key, which you share. There are some good reasons for this:

  • Addresses are shorter than public keys. The public key remains private until it's actually used to spend from that address, giving another (controversial, but true) degree of security against future cryptographic attacks.

The key to creating a legacy address:

publicKeyHash = RIPEMD160( SHA256( publicKey ) )
data = networkPrefix + publicKeyHash
checksum = first 4 bytes of SHA256( SHA256( data ) )
address = Base58CheckEncode( data + checksum )
Enter fullscreen mode Exit fullscreen mode

That RIPEMD160(SHA256(x)) combo even has its own nickname: HASH160. You'll see it constantly when reading Bitcoin source code.

Type Mainnet prefix Testnet prefix
P2PKH (legacy) 1 m / n
P2SH 3 2
Native segwit (bech32) bc1 tb1
addr = pub.get_address()
print(addr.to_string())
Enter fullscreen mode Exit fullscreen mode

Because the hash of a compressed key differs from the hash of an uncompressed key, the same private key produces two different legacy addresses depending on which public-key form you hashed. Worth remembering if an old wallet ever "loses" a balance check the other form.

Segwit addresses: same idea, new encoding

Since the Segregated Witness upgrade, the recommended address format is native segwit, encoded with Bech32 instead of Base58Check. P2WPKH (witness pubkey hash) addresses on mainnet start with bc1, and tb1 on testnet. Under the hood it's still essentially "hash of the public key" just packaged differently, and cheaper to spend from thanks to the witness discount we'll get into in a later post.

There's also a "nested" variant, a segwit script wrapped inside a P2SH address created so older, segwit-unaware wallets could still send funds to segwit users. It looks just like a normal P2SH address (prefix 3 / 2) from the sender's point of view.

A quick word on wallets

Wallets generally fall into two camps:

  • Non-deterministic — a pile of independently generated random keys. Simple, but every new key means a fresh backup.
  • Deterministic (HD) — a single seed deterministically generates an entire tree of keys. Back up the seed once, and you can always regenerate every address you've ever used. This is what virtually every modern wallet does (BIP-32/39/44).

Vanity addresses, briefly

It's possible to grind random keys until the resulting address starts with a chosen string (1Kostas, for example). Each extra character multiplies the search space by roughly 58 (the size of the Base58 alphabet), so long vanity prefixes get expensive fast which is why dedicated vanity-mining pools exist, using clever elliptic-curve key addition so they never learn your final private key.

Wrapping up

The whole chain: random number - private key - public key via curve multiplication - address via hashing is what makes Bitcoin ownership verifiable without any central authority knowing who you are. Next time you generate an address in code, you'll know exactly what's happening at each step.

Top comments (0)