WIF → private key HEX (with checksum proof)
Take a Wallet Import Format string apart: Base58Check payload, version byte, the 32-byte key body, the optional compression flag and the 4-byte double-SHA256 checksum — then re-derive the whole key set for the detected network. Mainnet and testnet, compressed and uncompressed, are all accepted.
Enter a value above and press Convert. Not sure what to type? Press “Use example” to load the sample from the placeholder.
What this page computes
You paste a WIF — a Wallet Import Format string, the thing every wallet since 2011 expects in its "import private key" box. This page takes it apart byte by byte, verifies the checksum that protects it, recovers the 32-byte scalar, reads the compression flag and the network byte, and then re-runs the whole derivation chain on the recovered key.
WIF comes in four flavours and all four are accepted here: mainnet compressed (K…/
L…), mainnet uncompressed (5…), testnet compressed (c…) and
testnet uncompressed (9…). The network is detected from the version byte and reported, and
the address derivation follows the detected network rather than assuming mainnet.
1. What WIF is and why it was invented
In 2011 the only way to move a private key between two Bitcoin clients was to copy 64 hexadecimal characters by hand. That is a terrible transport format, for three reasons:
- No redundancy. A hex string contains nothing that says "this is the string I meant". Change one character and you get a different, perfectly valid 256-bit number, controlling a different (almost certainly empty) address. The failure is silent, and it only surfaces when the money is gone.
- Confusable glyphs.
0andO,1andlandIlook alike in most fonts — exactly the mistake a person makes when copying a key from a printed paper wallet. - No metadata. The bare scalar has nowhere to record which network it belongs to, or whether the matching public key should be the 33-byte compressed form or the 65-byte uncompressed form. Both facts change the resulting address.
The Wallet Import Format fixes all three. It wraps the key in Base58Check: a radix-58
alphabet that deliberately omits the four confusable characters 0, O,
I and l (and also +, / and =), plus a
4-byte checksum that makes almost every typo detectable before anything is derived.
| Property | WIF | Bare hex key |
|---|---|---|
| Characters | 51 or 52 | 64 |
| Alphabet | Base58: 1–9 A–Z a–z minus 0 O I l | hex: 0–9 a–f |
| Typo detection | 4-byte SHA256d checksum — a wrong character is rejected with an error | none — a wrong character is a different key |
| Records the network | yes, version byte 0x80 / 0xEF | no |
| Records compression | yes, trailing 0x01 | no |
| Safe to write on paper | designed for it | not really |
2. The exact byte layout
A WIF is nothing more than a Base58Check encoding of a short byte string. For the mainnet compressed
case the payload is 0x80 ‖ key ‖ 0x01, 34 bytes in total, and Base58Check appends the
4-byte checksum, giving 38 bytes which encode to 52 Base58 characters. Here is the real demo WIF taken
apart byte by byte:
| Offset | Bytes | Value (demo WIF) | Meaning |
|---|---|---|---|
| 0 | 1 | 80 | version / network byte — mainnet |
| 1 – 32 | 32 | 0824c314…07a171ca | the private key itself |
| 33 | 1 | 01 | compression flag (present only in the compressed form) |
| 34 – 37 | 4 | 3a0a0417 | checksum = first 4 bytes of SHA256(SHA256(payload)) |
Two rows deserve emphasis.
The version byte is network metadata, not part of the secret. It tells a wallet which chain the key is for, so that the address derived from it uses the right prefix. Change it and you have a valid WIF for a different network — the same 32 bytes, different addresses.
The trailing 0x01 is not part of the key. It is a one-byte flag that
answers a question the 32-byte scalar cannot answer by itself: when this key's public key is serialized,
should it be the 33-byte compressed form or the 65-byte uncompressed form? The flag is not a checksum,
not a counter and not padding. It has exactly one legal value — 0x01 means "compressed",
and its absence means "uncompressed". A 33-byte body ending in any other byte is malformed and is
refused. Determining the key, in code, is a length test plus a one-byte test:
rest = payload[1:] # strip the version byte
if len(rest) == 33 and rest[-1] == 0x01:
compressed = True; key = rest[:32]
elif len(rest) == 32:
compressed = False; key = rest
else:
reject("payload is neither 33 nor 34 bytes")
3. The checksum, and how it catches a typo
Base58Check computes the checksum over the payload — the version byte, the key and the flag — and appends its first four bytes to the string that gets Base58-encoded. Verification recomputes it and compares:
Because the checksum is part of what the decoder sees, a single wrong character almost always makes the two disagree. The probability that a random typo still matches is 1 in 232, about 1 in 4.3 billion. Here is the demo WIF and what a real near-miss looks like — these are computed values, not illustrations:
| Input | Payload the decoder sees | Checksum in the string | Required checksum | Result |
|---|---|---|---|---|
…GK5tVqG (correct) |
800824c314…171ca01 |
3a0a0417 |
3a0a0417 |
valid |
…GK5tVqH — last character G→H |
800824c314…171ca01 (unchanged!) |
3a0a0418 |
3a0a0417 |
rejected |
…GK5tVqF — last character G→F |
800824c314…171ca01 |
3a0a0416 |
3a0a0417 |
rejected |
| two characters swapped in the middle | 800824c314772b53…dd97265df501 |
3a0a0417 |
a completely different value | rejected |
| last character deleted (51 chars) | 02351b1dd8a0f380…cacf94704e84 |
6f5872d4 |
a completely different value | rejected |
an O typed into the middle |
not decodable at all | — | rejected | |
The first near-miss is the most instructive. Changing the final character of a Base58 string
only perturbs the last byte of the decoded data, so the payload comes out byte-for-byte identical and
only the checksum changes: 3a0a0417 became 3a0a0418, one bit different. The
key you would have had is still the right key; the string is still refused. That is the whole point of
putting the checksum on the end.
The O row shows the second layer of defence: Base58 has no O, so the string
cannot even be decoded. A mistyped paper wallet is caught by the alphabet before the checksum is ever
consulted.
4. Prefixes: which network, which form
The version byte is what the first character of the string ultimately reflects. All four variants of the demo key are listed here, each with its real computed checksum:
| Version byte | Form | Network | First char | Length | Demo key |
|---|---|---|---|---|---|
0x80 | uncompressed | mainnet | 5 | 51 | 5HssbguBnjbaUVZCsH9YuzUG7T4vYQf51iiodi4izdVy6aVNekU |
0x80 | compressed (+0x01) | mainnet | K / L | 52 | KwVYL77Lcs1ckM39NvGjrtfKhU4KKdR2iB9Wq2FSMN6rbGK5tVqG |
0xEF | uncompressed | testnet | 9 | 51 | 91eWBRijNxfiSZ4VVd3Tnb2Dm7RdhaCGMfakiLRELNF1saLqhym |
0xEF | compressed (+0x01) | testnet | c | 52 | cMrXo27C3vhsunWQmL5sEDAPKhMiz5WinDHywShwrUkrr1RkCJ4i |
Their checksums, all computed over the respective payloads: mainnet uncompressed
b6f3d7f5, mainnet compressed 3a0a0417, testnet uncompressed
7271604c, testnet compressed 892c3e23.
K or L, and not just one letter?
The first character of a Base58 string is the top of a big number, and the compressed mainnet
payload sits right at a boundary: the payload's leading byte is always 0x80, but the
value of the whole 34-byte number varies with the key, so the most significant Base58 digit
can land on K or on L. There is nothing to interpret there — both mean
exactly the same thing. Only the version byte carries meaning.
5. The compression flag is in the WIF, not in the key
This is the single most consequential fact on this page. The 32-byte scalar k does not
know, and cannot know, whether it should be used with a compressed or an uncompressed public key. That
choice lives in the WIF's trailing byte, and it changes the address — because the address is a hash of
the serialized public key, and the two serializations are different byte strings.
Compressed WIF — K…
KwVYL77Lcs1ckM39NvGjrtfKhU4KKdR2iB9Wq2FSMN6rbGK5tVqG
public key 02ff812e…67ee80 (33 bytes)
hash160 21f57b6debcfc5b67182dfb4af1641f0a8189012
canonical address 146ZNjH7XTXnMd1ofe3z7CL2i8WM7N2UqT
Uncompressed WIF — 5…
5HssbguBnjbaUVZCsH9YuzUG7T4vYQf51iiodi4izdVy6aVNekU
public key 04ff812e…b424ae (65 bytes)
hash160 3f0e966dc089c611a9c1d03bd4f69ea53b0223f9
canonical address 16kR3eiswUY6tmGnxZhwQmW6LAvTLGWG2G
Same scalar, two strings, two addresses. Both are spendable with the same key; only a wallet that
knows both forms will find both. Note too that the SegWit and Taproot address types
(bc1q…, bc1p…) are always built from the compressed public key, because
witness programs are defined only over compressed keys — the uncompressed form is a legacy path that
only ever produces a P2PKH address.
If you import the compressed WIF but the coins were received at the uncompressed address, the wallet will look at the wrong address and show an empty balance. The reverse case is just as common with keys exported before 2012. Do not conclude the funds are lost: derive both forms — which is exactly what the result panel below does — and see which address actually holds them. Also note that you cannot "fix" this by editing the flag and recomputing the checksum: that produces a valid WIF whose address is the other one, which is the correct behaviour but not a recovery.
6. Worked example
For the sample WIF KwVYL77Lcs1ckM39NvGjrtfKhU4KKdR2iB9Wq2FSMN6rbGK5tVqG:
| Step | Value |
|---|---|
| Length | 52 Base58 characters, 38 decoded bytes |
| Decoded payload (34 bytes) | 800824c314772b2c2859c889471390a36bf66e53a1ca1642f96f0f2c9307a171ca01 |
| Version byte | 0x80 → Bitcoin mainnet |
| Key body (32 bytes) | 0824c314772b2c2859c889471390a36bf66e53a1ca1642f96f0f2c9307a171ca |
| Compression suffix | 0x01 → compressed |
| SHA256(SHA256(payload)) | 3a0a0417ffa98881ccdd0c7b283639584e0e909389f6bfbbdbac54547d2b732d |
| Checksum carried in the string | 3a0a0417 — matches the first 4 bytes above |
| Recovered scalar | 0824c314…07a171ca, in range [1, n−1] |
| Compressed public key | 02ff812e26116a9aa140abe629e7f8a38401653caf925119def59ed3758c67ee80 |
| Canonical P2PKH address | 146ZNjH7XTXnMd1ofe3z7CL2i8WM7N2UqT |
| P2SH-P2WPKH address | 3DZT76KyKyc5zkzvLugP8umgt4RPY4XPQw |
| P2WPKH address | bc1qy86hkm0telzmvuvzm76279jp7z5p3yqjnaerk6 |
| P2TR address | bc1pmsrhxnvmj8tjxxu9wj69ntvwvhvr28qufyc26tshz3rjj777tx3sh54u63 |
Re-encoding the recovered key on the other network gives
cMrXo27C3vhsunWQmL5sEDAPKhMiz5WinDHywShwrUkrr1RkCJ4i, whose addresses are
micWfnN6LUy38jVRPD2Mw7YMa873zjixGw (compressed) and
mmGNLhorkVyMfskQg8gKEgiRCAXAGnxeBv (uncompressed). Identical secret, four different
"first addresses" depending on two bytes of metadata.
7. Security and interoperability notes
- A WIF is the private key. It is a plain encoding with no encryption whatsoever.
Anyone who sees the string — in a screenshot, a chat log, a clipboard manager, a backup file — can
spend the coins. Encrypted exports exist as a separate format (BIP38 keys start with
6P), but a WIF is never encrypted. - The checksum is not authentication. It detects accidental typos; it does not
prove who created the string. Anyone can change the version byte and recompute a perfectly valid
checksum — this page shows such a string (
0x81) as "unknown prefix" rather than pretending it is a real network. - Network confusion is silent. Testnet and mainnet use different version bytes and different address prefixes. Importing a testnet WIF into mainnet software derives mainnet addresses, and the key is real — you simply will not find testnet coins at it.
- Never retype a WIF by hand if you can avoid it. The checksum catches one character being wrong; it cannot catch a correct-looking string that you copied from the wrong place. Compare the recovered hex against your source when it matters.
8. Common mistakes
- Treating WIF and hex as the same string. A WIF is 51 or 52 Base58 characters; hex is 64. Neither the alphabet nor the length matches.
- Assuming a
K…key is "newer" or "better". The letter is a consequence of the version byte and the key's numeric value; the information is in the flag byte, not in the first letter. - Editing the compression flag. Append or remove
0x01, recompute the checksum, and you get a valid WIF pointing at the other address — not a conversion of the first one. - Stripping the
0x01and expecting the same address. It will not be the same address. That is the entire content of section 5. - Forgetting that the version byte is inside the checksum's input. Change the network byte and the checksum must be recomputed too, which is precisely why you cannot hand-edit a WIF.
- Looking for padding. Base58Check has no
=and no fixed block size. A=in a supposed WIF means it is not a WIF at all.
9. Quick reference
| Item | Value |
|---|---|
| Payload | version ‖ key or version ‖ key ‖ 0x01 |
| Payload length | 33 bytes (uncompressed) or 34 bytes (compressed) |
| Checksum | first 4 bytes of SHA256(SHA256(payload)) |
| Total decoded length | 37 or 38 bytes |
| String length | 51 or 52 characters |
| Mainnet version byte | 0x80 → 5… uncompressed, K…/L… compressed |
| Testnet version byte | 0xEF → 9… uncompressed, c… compressed |
| Compression flag | 0x01 present = compressed; absent = uncompressed |
| Encrypted? | No. A WIF is plaintext. |
| Reversible? | Yes, with no key — decoding is all that is required |