Bitcoin Private Key Format: WIF, Base58 and Hex Explained
A Bitcoin private key is a 256-bit number. The string most wallets show for it is that number in Wallet Import Format (WIF): a version byte, the key, an optional compression flag and a 4-byte checksum, all written in Base58. This guide takes one key through every step of the encoding, shows how to read a WIF string at a glance, and explains what its checksum can and cannot protect you from. Every key below comes from a published test vector and is public knowledge: never send coins to the addresses they control.
What a Private Key Is
A private key is any number from 1 to n − 1, where n is the order of secp256k1, the elliptic curve Bitcoin uses:
n = FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE BAAEDCE6 AF48A03B BFD25E8C D0364141
As raw hexadecimal, a key is 64 characters (32 bytes). The public key and the address are calculated from it, never the other way round. In a modern wallet you rarely meet a key on its own: an HD wallet derives every key from the seed phrase through BIP32, and the guide to exporting a private key shows which wallets let you see one. When they do, they usually show it in WIF.
The Base58 Alphabet
Base58 uses 58 characters: all digits and Latin letters except 0 (zero), O (capital o), I (capital i) and l (lowercase L).
| Group | Characters | Count |
|---|---|---|
| Digits | 123456789 | 9 |
| Uppercase | ABCDEFGHJKLMNPQRSTUVWXYZ | 24 |
| Lowercase | abcdefghijkmnopqrstuvwxyz | 25 |
Bitcoin Core’s base58.h explains the choice: the excluded characters look alike in many fonts, and a string without punctuation is easier to accept as input, survives email line-wrapping and is selected whole by a double-click. Each leading zero byte is written as 1, which is why legacy addresses, whose version byte is 0x00, start with 1.
How WIF Encoding Works, Step by Step
The example key is the master private key of BIP32 test vector 1 (seed 000102…0f), read from the xprv9s21ZrQH143K3QTDL4… string published in BIP32:
- Start with the 32-byte key:
e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35. - Add the version byte
0x80in front for mainnet (0xeffor testnet). - Add the compression flag
0x01at the end if the key is used with a compressed public key, the default in current wallets. - Compute the checksum: hash the bytes with SHA-256 twice and keep the first 4 bytes.
- Append the checksum and encode the whole byte string in Base58. This combination of version byte, double-SHA-256 checksum and Base58 is called Base58Check.
| Compressed | Uncompressed | |
|---|---|---|
| Payload | 80 + key + 01 (34 bytes) | 80 + key (33 bytes) |
| Checksum | f387029f | 2bf720ff |
| WIF | L52XzL2cMkHxqxBXRyEpnPQZGUs3uKiL3R11XbAdHigRzDozKZeW | 5KasyVKwgbH5VmDomdJdevZXRMMrbWcePkW17vxeg8daJWoeqHQ |
| Length | 52 characters | 51 characters |
The same steps in Python, using only the standard library, plus a decoder that reverses them:
import hashlib
B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def to_wif(key_hex: str, compressed: bool = True, testnet: bool = False) -> str:
payload = (b"\xef" if testnet else b"\x80") + bytes.fromhex(key_hex)
if compressed:
payload += b"\x01"
payload += hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
n, out = int.from_bytes(payload, "big"), ""
while n:
n, r = divmod(n, 58)
out = B58[r] + out
return out # the payload starts with 0x80 or 0xef, so no leading "1" is needed
def from_wif(wif: str) -> tuple[str, bool]:
n = 0
for ch in wif:
n = n * 58 + B58.index(ch)
raw = n.to_bytes(38 if len(wif) == 52 else 37, "big")
payload, checksum = raw[:-4], raw[-4:]
if hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] != checksum:
raise ValueError("checksum mismatch: the key was mistyped")
return payload[1:33].hex(), len(payload) == 34 # (hex key, compressed?)
key = "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35"
print(to_wif(key)) # L52XzL2cMkHxqxBXRyEpnPQZGUs3uKiL3R11XbAdHigRzDozKZeW
print(to_wif(key, compressed=False)) # 5KasyVKwgbH5VmDomdJdevZXRMMrbWcePkW17vxeg8daJWoeqHQ
Reading a WIF String at a Glance
Because the version byte and the length are fixed, the first character and the length tell you the network and the compression flag:
| Starts with | Length | Network | Public key |
|---|---|---|---|
5 | 51 | Mainnet | Uncompressed |
K or L | 52 | Mainnet | Compressed |
9 | 51 | Testnet | Uncompressed |
c | 52 | Testnet | Compressed |
A 64-character hex string is the same key without any of this framing. A 111-character string starting with xprv is not a single key but a BIP32 extended private key, which derives a whole branch of keys and also uses Base58 with a checksum.
Compressed or Uncompressed: Why the Flag Matters
The flag does not shrink the private key. It tells the wallet which public key to calculate: the 65-byte uncompressed form or the 33-byte compressed form. The two forms hash to different legacy addresses, so a key imported with the wrong flag shows a different, usually empty, address. SegWit needs the compressed form: under BIP143, transactions that use uncompressed keys in version 0 witness programs are not relayed or mined by default.
WIF does not record the address type either: one compressed key controls a legacy (1…), a nested SegWit (3…) and a native SegWit (bc1q…) address, each with its own balance (address types explained). Electrum therefore writes a script type such as p2wpkh: in front of the keys it exports. Bitcoin Core 30.0 removed the old importprivkey and dumpprivkey RPCs (release notes); a descriptor wallet takes the key inside a descriptor that names the type, such as wpkh(<WIF>), imported with importdescriptors and the checksum that getdescriptorinfo returns.
What the Checksum Catches, and What It Doesn’t
The checksum is 32 bits, so a randomly corrupted key passes it only about once in 4.3 billion tries (1 in 2^32). Change one character of L52Xz…KZeW and from_wif raises an error instead of returning a different key. That protects you from importing a mistyped key, but the checksum only says that something is wrong, not where. It also does nothing for secrecy: a WIF string is the key itself, and anyone who sees it, photographs it or receives it in a chat can spend the coins it controls.
A Test Vector from a Seed Phrase
To follow the chain from words to address, take the public BIP39 test phrase (abandon eleven times, then about) and the path m/84’/0’/0’/0/0. BIP84 publishes the result, and we reproduced it from the phrase:
| Field | Value |
|---|---|
| Private key (WIF) | KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d |
| Private key (hex) | 4604b4b710fe91f584fff084e1a9159fe4f8408fff380596a604948474ce4fa3 |
| Public key | 0330d54fd0dd420a6e5f8d3624f5f3482cae350f79d5f0753bf5beef9c2d91af3c |
| Address | bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu |
The WIF starts with K, so it is a compressed mainnet key, as a native SegWit address requires.
Base58 vs Base64 vs Bech32
| Base58 with checksum | Base64 | Bech32 / Bech32m | |
|---|---|---|---|
| Alphabet | 58 characters, no 0, O, I, l | 64 characters including + and /, = as padding | 32 characters, lowercase, no 1, b, i, o |
| Checksum | 4 bytes of double SHA-256 | None | BCH code that always detects up to 4 mistyped characters |
| Used in Bitcoin for | WIF keys, legacy and P2SH addresses, xprv and xpub | PSBT files exchanged as text (BIP174) | Native SegWit (bc1q…) and Taproot (bc1p…) addresses |
Bech32 (BIP173) replaced Base58 for SegWit addresses: its single-case alphabet is easier to read aloud and type, and its checksum guarantees that small typos are caught.
Handling WIF Keys Safely
- Treat a WIF like cash. It cannot be changed or revoked, so if it may have been seen, move the funds to a new wallet. Never paste it into a website, chat or “support” form.
- Sweep instead of importing when a wallet offers it, so the coins end up under the receiving wallet’s own backup.
- Generate keys locally. Our private key generator creates them in your browser.
Frequently Asked Questions
Is a WIF private key the same as a seed phrase?
No. A WIF string is one key and controls only the addresses calculated from it. A seed phrase recreates every key in an HD wallet. You cannot get the seed phrase back from a WIF key.
How do I convert a WIF key to hex?
Decode the Base58 string, check the 4-byte checksum, then drop the version byte, the checksum and the 01 flag if present. The remaining 32 bytes are the hex key; from_wif above does exactly that. Do it offline.
Why does my imported key show an empty balance?
Usually the wallet calculated a different address from the one that holds the coins: the compression flag differs, or the wallet assumed another address type. Look up the funded address in a block explorer and import the key with the matching type, such as Electrum’s p2wpkh: prefix or a pkh(), sh(wpkh()) or wpkh() descriptor in Bitcoin Core.