Skip to content

Hot Wallets vs Cold Wallets

💡
Before you start

You need Python 3, one library, and about twenty minutes. On Windows, install Python from python.org with “Add python.exe to PATH” ticked; macOS and Linux already have it. Then run pip install cryptography once and confirm with python3 -c "import cryptography; print(cryptography.__version__)", which should print a version number.

You do not need two computers, a hardware wallet, or any cryptocurrency. Two folders on one machine will stand in for the two machines — one that touches the internet and one that never does. The rule that makes the exercise honest is simple and you must keep it: one named file, and only that file, ever moves between the folders at each step. That single restriction is the whole of cold storage.

The Hot and Cold Distinction

The terms "hot" and "cold" describe whether a wallet is connected to the internet:

Hot Wallet Connected to the internet. Your private keys exist on a device with network access. Convenient for frequent transactions but exposed to online threats.
Cold Wallet Not connected to the internet. Your private keys are stored offline, making remote hacking virtually impossible. Less convenient but far more secure for long-term storage.

Types of Hot Wallets

  • Mobile apps (BlueWallet, Trust Wallet) — convenient for daily spending, vulnerable to phone malware and theft
  • Desktop software (Electrum, Exodus) — more features, vulnerable to PC malware, keyloggers, and clipboard hijackers
  • Browser extensions (MetaMask) — required for DeFi, highest risk due to browser attack surface, malicious sites, and phishing
  • Exchange accounts — technically custodial hot wallets managed by the exchange on your behalf

Types of Cold Wallets

  • Hardware wallets (Ledger, Trezor) — dedicated devices that sign transactions offline. The private key never touches an internet-connected device. The gold standard for security.
  • Paper wallets — private keys printed on paper. Completely offline, but fragile (fire, water, ink fading) and awkward to use for transactions.
  • Air-gapped computers — a computer that has never been and will never be connected to the internet. Used to generate and store keys. Transactions are signed offline and transferred via USB or QR code.
  • Steel/metal seed storage — seed phrase stamped or engraved into metal plates. Resistant to fire, water, and physical degradation. Not a wallet itself, but a cold backup of your keys.

Security Comparison

Remote hacking Hot wallets are vulnerable. Cold wallets are immune (keys never touch the internet).
Malware Hot wallets can be compromised by keyloggers, clipboard hijackers, and screen capture malware. Hardware wallets display transaction details on their own screen for verification.
Phishing Hot wallets (especially browser extensions) are prime phishing targets. Hardware wallets require physical button presses to confirm transactions, adding a layer of protection.
Physical theft Hot wallets on stolen devices may be accessible if not properly encrypted. Hardware wallets are protected by a PIN and are useless to a thief without it.
Convenience Hot wallets allow instant transactions. Cold wallets require connecting the device or using an air-gap transfer process.

When to Use Each

💡
Think of it like physical money

A hot wallet is like a wallet in your pocket — keep enough for daily spending. A cold wallet is like a safe — keep your savings there. You would not carry your life savings in your back pocket.

  • Hot wallet: Small amounts for regular transactions, DeFi interaction, testing and learning
  • Cold wallet: Long-term savings, any amount you cannot afford to lose, amounts exceeding the cost of a hardware wallet
  • Both: Most experienced users keep a small hot wallet for convenience and a hardware wallet for the majority of their holdings

Now Run a Cold Wallet Yourself, in Five Steps

Cold storage sounds like hardware. It is not — it is a rule about which file is allowed to move. In the next twenty minutes you will set up a cold machine and a hot machine as two folders, create a key that never leaves the cold one, sign a real payment there, verify it on the hot side, and then play the attacker: read every single byte the hot machine holds and try to steal the money anyway. Every line of output below came from running these commands in that order.

1
Build the two machines, and create the key on the cold one

Go: open a terminal in a folder you can write to and make the layout: mkdir -p airgap/cold airgap/hot, then cd airgap/cold.

Do: save this as make_key.py inside airgap/cold and run python3 make_key.py.

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
import hashlib, os

key = Ed25519PrivateKey.generate()
raw = key.private_bytes(serialization.Encoding.Raw,
                        serialization.PrivateFormat.Raw,
                        serialization.NoEncryption())
pub = key.public_key().public_bytes(serialization.Encoding.Raw,
                                    serialization.PublicFormat.Raw)

open("cold_secret.key", "wb").write(raw)
os.chmod("cold_secret.key", 0o600)
open("public_address.txt", "w").write(pub.hex())

print("secret written to cold_secret.key  (never copy this file anywhere)")
print("address written to public_address.txt")
print("address:", hashlib.sha256(pub).hexdigest()[:40])

You should see: three lines, the last one a 40-character address. Your address will not match the one below — it is made from fresh randomness every time, which is exactly what you want from a real wallet:

secret written to cold_secret.key  (never copy this file anywhere)
address written to public_address.txt
address: c2defea5bb2232f9732bc1cc8ad6dc04069b6368

Check the permissions with ls -l cold_secret.key (Windows: icacls cold_secret.key). On macOS or Linux the mode column should read -rw-------: readable by you and by nobody else on the machine.

If not: ModuleNotFoundError: No module named 'cryptography' means the library is missing — run pip install cryptography. AttributeError: module 'os' has no attribute 'chmod' cannot happen on a normal install; if os.chmod silently does nothing, you are on Windows, where file permissions work differently — use the folder’s Properties → Security tab instead, or simply carry on, since nothing of value is in this practice key.

2
Move the public half across — and nothing else, ever

Go: back up one level: cd .. so you are in airgap and can see both folders.

Do: copy exactly one file, then build an unsigned payment on the hot side.

cp cold/public_address.txt hot/
cd hot

Now save this as make_payment.py in airgap/hot and run python3 make_payment.py:

import json

payment = {"to": "merchant-7fa31c", "amount": "0.25", "nonce": 41}
open("unsigned.json", "w").write(json.dumps(payment, sort_keys=True))
print("unsigned payment written to unsigned.json")
print(open("unsigned.json").read())

You should see: the payment, byte for byte as below — this one is fixed, so yours will match:

unsigned payment written to unsigned.json
{"amount": "0.25", "nonce": 41, "to": "merchant-7fa31c"}

That copy step is the real air gap. In a hardware wallet the same transfer happens over a USB cable or a QR code, and the same asymmetry holds: the public address goes out, the secret never does.

If not: cp: cannot stat 'cold/public_address.txt' means you are not in the airgap folder — run pwd (Windows: cd) and check. On Windows use copy cold\public_address.txt hot\.

3
Sign it on the cold machine, which never sees the network

Go: cd ../cold. Carry the payment across by hand: cp ../hot/unsigned.json .

Do: save this as sign_offline.py and run it. This is the only moment the secret is used, and it happens on the machine that has no internet.

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

payment = open("unsigned.json", "rb").read()
key = Ed25519PrivateKey.from_private_bytes(open("cold_secret.key", "rb").read())
open("signature.txt", "w").write(key.sign(payment).hex())

print("signed:", payment.decode())
print("signature.txt written -- copy ONLY this file back")

You should see: the exact payment you approved, and one new file:

signed: {"amount": "0.25", "nonce": 41, "to": "merchant-7fa31c"}
signature.txt written -- copy ONLY this file back

Read that first line again, because it is the security control people skip on real hardware. A hardware wallet shows you the amount and destination on its own screen for precisely this reason: the hot machine may be lying about what it asked you to sign. Confirming on the trusted device is not a formality — it is the check.

If not: FileNotFoundError: 'unsigned.json' means the copy step was skipped. ValueError: An Ed25519 private key is 32 bytes long means cold_secret.key was opened in text mode somewhere and mangled — delete it, re-run step 1, and redo this step.

4
Verify on the hot machine — and try to change the amount

Go: cd .. then bring back the one file that is safe to move: cp cold/signature.txt hot/ and cd hot.

Do: save this as broadcast.py and run it. It checks the signature, then quietly tries to raise the amount from 0.25 to 9.75 — the exact thing a compromised wallet app would attempt.

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

pub = Ed25519PublicKey.from_public_bytes(
    bytes.fromhex(open("public_address.txt").read().strip()))
payment = open("unsigned.json", "rb").read()
sig = bytes.fromhex(open("signature.txt").read().strip())

try:
    pub.verify(sig, payment)
    print("SIGNATURE VALID -- safe to broadcast:", payment.decode())
except InvalidSignature:
    print("SIGNATURE INVALID -- do not broadcast")

tampered = payment.replace(b'"0.25"', b'"9.75"')
try:
    pub.verify(sig, tampered)
    print("tampered amount ACCEPTED")
except InvalidSignature:
    print("tampered amount REJECTED:", tampered.decode())

You should see: the genuine payment accepted, the altered one refused:

SIGNATURE VALID -- safe to broadcast: {"amount": "0.25", "nonce": 41, "to": "merchant-7fa31c"}
tampered amount REJECTED: {"amount": "9.75", "nonce": 41, "to": "merchant-7fa31c"}

The signature is bound to that payment and no other. Nothing on the hot machine can edit an approved transaction — it can only refuse to send it.

If not: SIGNATURE INVALID on the first line means unsigned.json in hot and the one you signed in cold are not identical — re-copy it and re-sign. ValueError: non-hexadecimal number found means signature.txt picked up a newline; the .strip() should handle it, so check the copy actually completed.

5
Become the attacker: own the hot machine completely, and still fail

Go: stay in airgap/hot. Assume malware now has full read access to this folder — which is what “your PC was infected” means.

Do: first prove the secret is genuinely not here, then run the theft. Save this as search_secret.py and run it — it reads the real key from the cold folder and hunts for those exact bytes in every file the hot machine has.

import os

secret = open("../cold/cold_secret.key", "rb").read()
found = []
for name in sorted(os.listdir(".")):
    if os.path.isfile(name) and secret in open(name, "rb").read():
        found.append(name)
print("bytes of the cold secret found in:", found or "NOTHING on this machine")

Then run the same file from inside ../cold (change the path in it to "cold_secret.key") so you can see the search is capable of finding something — a search that never finds anything proves nothing.

Now save this as malware.py:

import os
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

print("files this malware can read:", sorted(os.listdir(".")))

pub = Ed25519PublicKey.from_public_bytes(
    bytes.fromhex(open("public_address.txt").read().strip()))
stolen_sig = bytes.fromhex(open("signature.txt").read().strip())

theft = b'{"amount": "9.75", "nonce": 41, "to": "attacker-wallet"}'
try:
    pub.verify(stolen_sig, theft)
    print("THEFT SUCCEEDED")
except InvalidSignature:
    print("THEFT FAILED: the stolen signature does not cover this payment")

You should see: the search coming up empty on the hot machine and finding the key on the cold one, then the attacker reading everything and getting nowhere:

bytes of the cold secret found in: NOTHING on this machine
bytes of the cold secret found in: ['cold_secret.key']
files this malware can read: ['broadcast.py', 'make_payment.py', 'malware.py', 'public_address.txt', 'search_secret.py', 'signature.txt', 'unsigned.json']
THEFT FAILED: the stolen signature does not cover this payment

This is the entire argument for cold storage in one line of output. The attacker has the address, a valid signature, the payment file and the code — total compromise of the hot machine — and cannot move a coin, because a signature proves one specific payment and cannot be re-aimed at another. A hot wallet holding the key would have lost everything at the moment of infection.

If not: if the search prints a filename on the hot machine, you copied something you should not have — delete the whole hot folder and repeat from step 2, moving only the two files named there. If THEFT SUCCEEDED ever prints, the theft line was edited to match the original payment; put it back exactly as written.

🎉
Check yourself before moving on

Without scrolling up: in step 5 the attacker held a genuine signature and still failed. So what would an attacker who fully controls the hot machine actually be able to do to you? Answer: three things, none of which is spending your existing coins. They can watch your balance and addresses, they can refuse to broadcast or delay your transactions, and — the dangerous one — they can show you a payment screen that differs from the transaction they actually hand to the cold device. That last attack is defeated only by reading the amount and destination on the cold device’s own screen before approving, which is why hardware wallets have screens at all.

Now do it without the page: repeat the whole cycle for a second payment, this time with "nonce": 42, and deliberately try to broadcast it using step 4’s old signature.txt. Predict the result before you run it, then confirm. You have just demonstrated why every transaction carries a nonce — and why replaying an old signed transaction is not an attack that works.

Summary

  • Hot wallets are internet-connected and convenient but expose keys to online threats
  • Cold wallets store keys offline, making remote attacks impossible
  • Hardware wallets are the most practical form of cold storage for most users
  • Use hot wallets for spending money and cold wallets for savings
  • The best approach combines both: small amounts hot, savings cold
🎉
You understand the hot vs cold tradeoff!

Ready to secure your assets? Learn how to set up a hardware wallet in the next tutorial.