Have you ever wondered how cryptographic puzzles like the famous Bitcoin Challenge are actually solved? Or how raw mathematical operations transform a random 256-bit number into a valid Bitcoin address?
Today, we're doing a code teardown of a Python script designed to brute-force specific Bitcoin key spaces. While brute-forcing a standard 256-bit Bitcoin private key is mathematically impossible (there are more possible keys than atoms in the observable universe), scripts like this are used to hunt for lost keys in restricted key spaces or solve known cryptographic puzzles.
Before we break down the Elliptic Curve Cryptography (ECC), data encoding, and high-performance concurrent processing, here is the complete, working script.
The Complete Code: walletworld.py
Save this as walletworld.py. For a massive speedup, make sure to run pip install coincurve before executing it.
(Note: The code below has been cleaned of any non-breaking spaces so it is copy-paste ready and executable).
#!/usr/bin/env python3
"""
walletworld.py – fixed, faster, smarter funded-target hunter
Improvements over original:
• single EC multiplication for both compressed/uncompressed
• proper argparse CLI
• multiprocessing for --scan
• progress + ETA
• concurrent balance lookups
• cleaner target handling
Requires: pip install coincurve (strongly recommended)
Optional: pip install tqdm
"""
from __future__ import annotations
import argparse
import hashlib
import json
import multiprocessing as mp
import sys
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Dict, List, Optional, Set, Tuple
# ------------------------------------------------------------------ curve
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
G = (Gx, Gy)
def _add(a: Optional[Tuple[int, int]], b: Optional[Tuple[int, int]]) -> Optional[Tuple[int, int]]:
if a is None:
return b
if b is None:
return a
x1, y1 = a
x2, y2 = b
if x1 == x2:
if (y1 + y2) % P == 0:
return None
lam = (3 * x1 * x1) * pow(2 * y1, P - 2, P) % P
else:
lam = (y2 - y1) * pow((x2 - x1) % P, P - 2, P) % P
x3 = (lam * lam - x1 - x2) % P
y3 = (lam * (x1 - x3) - y1) % P
return (x3, y3)
def _mul(k: int, pt: Tuple[int, int]) -> Optional[Tuple[int, int]]:
r = None
while k:
if k & 1:
r = _add(r, pt)
pt = _add(pt, pt)
k >>= 1
return r
try:
import coincurve
def pubkeys_from_priv(k: int) -> Tuple[bytes, bytes]:
"""Return (uncompressed, compressed) public keys."""
pk = coincurve.PublicKey.from_valid_secret(k.to_bytes(32, "big"))
return pk.format(False), pk.format(True)
ENGINE = "coincurve"
except Exception:
def pubkeys_from_priv(k: int) -> Tuple[bytes, bytes]:
pt = _mul(k, G)
if pt is None:
raise ValueError("invalid private key")
x, y = pt
uncomp = b"\x04" + x.to_bytes(32, "big") + y.to_bytes(32, "big")
comp = bytes([2 + (y & 1)]) + x.to_bytes(32, "big")
return uncomp, comp
ENGINE = "pure-python"
# --------------------------------------------------------------- encoding
B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def b58encode(b: bytes) -> str:
n = int.from_bytes(b, "big")
out = []
while n:
n, r = divmod(n, 58)
out.append(B58[r])
pad = len(b) - len(b.lstrip(b"\x00"))
return "1" * pad + "".join(reversed(out))
def b58check(payload: bytes) -> str:
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
return b58encode(payload + checksum)
def hash160(b: bytes) -> bytes:
return hashlib.new("ripemd160", hashlib.sha256(b).digest()).digest()
# ---------------------------------------------------------------- bech32
_CH = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def _polymod(values: List[int]) -> int:
GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]
chk = 1
for v in values:
b = chk >> 25
chk = ((chk & 0x1ffffff) << 5) ^ v
for i in range(5):
if (b >> i) & 1:
chk ^= GEN[i]
return chk
def _hrp_expand(hrp: str) -> List[int]:
return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]
def _convertbits(data: bytes, frombits: int, tobits: int, pad: bool = True) -> List[int]:
acc = 0
bits = 0
ret = []
maxv = (1 << tobits) - 1
for value in data:
acc = (acc << frombits) | value
bits += frombits
while bits >= tobits:
bits -= tobits
ret.append((acc >> bits) & maxv)
if pad and bits:
ret.append((acc << (tobits - bits)) & maxv)
return ret
def p2wpkh(pubkey: bytes) -> str:
witprog = hash160(pubkey)
data = [0] + _convertbits(witprog, 8, 5)
combined = data + [0] * 6
polymod = _polymod(_hrp_expand("bc") + combined) ^ 1
checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
return "bc1" + "".join(_CH[d] for d in data + checksum)
def derive(k: int) -> Dict[str, str]:
if not (1 <= k < N):
raise ValueError("private key out of range")
uncomp, comp = pubkeys_from_priv(k)
hb = k.to_bytes(32, "big")
return {
"hex": f"{k:064x}",
"wif_u": b58check(b"\x80" + hb),
"wif_c": b58check(b"\x80" + hb + b"\x01"),
"addr_u": b58check(b"\x00" + hash160(uncomp)),
"addr_c": b58check(b"\x00" + hash160(comp)),
"segwit": p2wpkh(comp),
}
# ---------------------------------------------------------------- balance
def balance(addr: str) -> Optional[int]:
"""Confirmed + mempool sats. None on failure."""
try:
url = f"https://mempool.space/api/address/{addr}"
with urllib.request.urlopen(url, timeout=12) as r:
d = json.load(r)
return sum(
d[s]["funded_txo_sum"] - d[s]["spent_txo_sum"]
for s in ("chain_stats", "mempool_stats")
)
except Exception:
return None
# ------------------------------------------------------- targets & verify
VERIFY_KEYS = [1, 3, 7, 8, 21, 49, 76, 224, 467, 514, 1155] # OEIS A369920
FUNDED = [
"1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", # genesis
"12c6DSiU4Rq3P4ZxziKxzrL5LmMBrzjrJX", # block 1
"1HLoD9E4SDFFPDiYfNYnkBLQ85Y51J3Zb1", # block 9 (Hal Finney)
"1BitcoinEaterAddressDontSendf59kuE",
"1KfZGvwZxsvSmemoCmEV75uqcNzYBHjkHZ", # Brave New World puzzle
"1PWo3JeB9jrGwfHDNpdGK54CRas7fsVzXU", # puzzle #71
"1JTK7s9YVYywfm5XUH7RNhHJH1LshCaRFR", # puzzle #72
"12VVRNPi4SJqUTsp6FmqDqY5sGosDtysn4", # puzzle #73
"1FWGcVDK3JGzCC3WtkYetULPszMaK2Jksv", # puzzle #74
"189KtDXinEgnfp3tiHyytGvhPzJMmVt3vM",
]
# ------------------------------------------------------------------ helpers
def selftest() -> None:
v = derive(1)
assert v["wif_u"] == "5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf"
assert v["wif_c"] == "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn"
assert v["addr_u"] == "1EHNa6Q4Jz2uvNExL497mE43ikXhwF6kZm"
assert v["addr_c"] == "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"
print(f"[ok] self-test passed (engine={ENGINE})")
def report_hit(k: int, fmt: str, addr: str, known: bool = False) -> None:
d = derive(k)
bal = balance(addr)
print("=" * 64)
print("HIT", "|", fmt, "|", "known key" if known else "funded target")
if bal is not None:
print(f" balance : {bal:,} sats ({bal / 1e8:.8f} BTC)")
print(" priv hex:", d["hex"])
print(" wif (u) :", d["wif_u"])
print(" wif (c) :", d["wif_c"])
print(" address :", addr)
print(" explorer: https://mempool.space/address/" + addr)
print("=" * 64)
# Shared state for workers
_TARGETS: Set[str] = set()
_KNOWN: Dict[str, int] = {}
def _worker_init(targets: Set[str], known: Dict[str, int]) -> None:
global _TARGETS, _KNOWN
_TARGETS = targets
_KNOWN = known
def _check_key(k: int) -> Optional[Tuple[int, str, str, bool]]:
"""Return (k, fmt, addr, is_known) on hit, else None."""
try:
d = derive(k)
except Exception:
return None
for fmt in ("addr_u", "addr_c", "segwit"):
addr = d[fmt]
if addr in _TARGETS:
return (k, fmt, addr, addr in _KNOWN)
return None
# -------------------------------------------------------------------- main
def main() -> None:
parser = argparse.ArgumentParser(
description="Bitcoin funded-target key hunter (secp256k1)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s --verify
%(prog)s --filter-funded
%(prog)s --key 1
%(prog)s --scan 1 1000000 --workers 8
%(prog)s --add 1ABC... --scan 2**40 2**40+1000000
""",
)
parser.add_argument("--verify", action="store_true",
help="scan 1..2001 against known keys (self-check)")
parser.add_argument("--known", action="store_true",
help="print the verify-key address table")
parser.add_argument("--key", metavar="HEX",
help="derive a single private key (hex)")
parser.add_argument("--filter-funded", action="store_true",
help="keep only currently funded targets")
parser.add_argument("--add", metavar="ADDR", action="append", default=[],
help="add extra target address (repeatable)")
parser.add_argument("--targets", metavar="FILE",
help="load extra targets from file (one per line)")
parser.add_argument("--scan", nargs=2, metavar=("LO", "HI"),
help="brute-force range [LO, HI)")
parser.add_argument("--workers", type=int, default=max(1, mp.cpu_count() - 1),
help="number of worker processes for --scan (default: cpu-1)")
parser.add_argument("--chunk", type=int, default=50_000,
help="keys per work unit (default: 50000)")
args = parser.parse_args()
selftest()
if args.known:
for k in VERIFY_KEYS:
print(f"key {k:>6} {derive(k)['addr_c']}")
return
if args.key:
try:
k = int(args.key, 16)
for name, val in derive(k).items():
print(f"{name:7}: {val}")
except ValueError as e:
print(f"bad key: {e}", file=sys.stderr)
sys.exit(1)
return
# Build target set
targets: Set[str] = set(FUNDED)
known: Dict[str, int] = {}
if args.targets:
try:
with open(args.targets) as fh:
targets.update(line.strip() for line in fh if line.strip())
except FileNotFoundError:
print(f"!! {args.targets} not found – continuing without it", file=sys.stderr)
for addr in args.add:
targets.add(addr)
if args.verify:
for k in VERIFY_KEYS:
addr = derive(k)["addr_c"]
known[addr] = k
targets.add(addr)
print(f"verify scan 1..2001 – expect {len(VERIFY_KEYS)} hits (engine={ENGINE})")
hits = 0
for k in range(1, 2002):
res = _check_key(k) # uses module-level empty sets; re-init below
# re-init for verify
_worker_init(targets, known)
res = _check_key(k)
if res:
hits += 1
report_hit(*res)
print(f"\n{hits}/{len(VERIFY_KEYS)} known keys recovered.")
return
if args.filter_funded:
print(f"checking {len(targets)} targets for balance …")
kept = []
with ThreadPoolExecutor(max_workers=12) as ex:
futs = {ex.submit(balance, t): t for t in sorted(targets)}
for fut in as_completed(futs):
t = futs[fut]
b = fut.result()
if b and b > 0:
kept.append((t, b))
print(f" keep {t} {b:,} sats")
if not kept:
print("none funded (or all lookups failed)")
return
print(f"\n{len(kept)} funded targets:")
for t, _ in kept:
print(t)
return
if args.scan:
try:
lo = int(args.scan[0], 0) # allow 0x / 2**n style later if wanted
hi = int(args.scan[1], 0)
except ValueError:
print("LO and HI must be integers", file=sys.stderr)
sys.exit(1)
if hi <= lo:
print("HI must be > LO", file=sys.stderr)
sys.exit(1)
if lo < 1:
lo = 1
if hi > N:
hi = N
total = hi - lo
print(f"scanning {lo}..{hi} ({total:,} keys, {len(targets)} targets, "
f"engine={ENGINE}, workers={args.workers})")
# Prepare shared state
_worker_init(targets, known)
start = time.time()
found = False
checked = 0
# Work units
chunk = max(1000, args.chunk)
ranges = [(i, min(i + chunk, hi)) for i in range(lo, hi, chunk)]
with mp.Pool(processes=args.workers, initializer=_worker_init,
initargs=(targets, known)) as pool:
# Use imap_unordered for responsiveness
for res in pool.imap_unordered(_check_key,
(k for start, end in ranges for k in range(start, end)),
chunksize=256):
checked += 1
if checked % 100_000 == 0 or checked == total:
elapsed = time.time() - start
rate = checked / elapsed if elapsed > 0 else 0
eta = (total - checked) / rate if rate > 0 else 0
print(f"\r {checked:,}/{total:,} {rate:,.0f} keys/s ETA {eta/60:.1f} min",
end="", flush=True)
if res:
print() # newline after progress
report_hit(*res)
found = True
pool.terminate()
break
if not found:
print("\ndone – no match")
else:
print(f"\nfinished after {checked:,} keys")
return
# Default: just show status
print(f"targets loaded: {len(targets)}")
print("modes: --verify --filter-funded --key HEX --scan LO HI --add ADDR")
print(f"engine: {ENGINE} (install coincurve for large speed-up)")
if __name__ == "__main__":
# Required on Windows / some platforms
mp.freeze_support()
main()
1. The Math: Elliptic Curve Cryptography (SECP256k1)
At the heart of Bitcoin is the secp256k1 elliptic curve. The script implements this in pure Python as a fallback, which is a fantastic educational look at how ECC math actually works.
P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798
Gy = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8
G = (Gx, Gy)
To derive a public key, you multiply the private key (a massive scalar) by the Generator point (G). The script uses a classic double-and-add algorithm (_mul) to perform this efficiently in modulo arithmetic:
def _mul(k: int, pt: Tuple[int, int]) -> Optional[Tuple[int, int]]:
r = None
while k:
if k & 1:
r = _add(r, pt)
pt = _add(pt, pt)
k >>= 1
return r
The Need for Speed
Pure Python is great for learning, but terrible for brute-forcing. Python's dynamic typing and global interpreter lock (GIL) make math-heavy operations slow. To solve this, the script cleverly falls back on pure Python only if the coincurve library (a blazing-fast C-binding for libsecp256k1) is unavailable.
try:
import coincurve
# Use blazing fast C-bindings
ENGINE = "coincurve"
except Exception:
# Fallback to pure Python math
ENGINE = "pure-python"
2. Deriving Addresses: Base58 and Bech32
Once you have the public key coordinates (x and y), you can't just use them as an address. They must be hashed and encoded. The script derives multiple address types simultaneously:
- Legacy Uncompressed (P2PKH): Starting with
1
- Legacy Compressed (P2PKH): Also starting with
1, but using a shorter public key representation.
- SegWit (P2WPKH): Native SegWit addresses starting with
bc1.
The encoding mechanisms are built from scratch. For Legacy addresses, it uses Base58Check encoding (which removes confusing characters like 0, O, l, and I). For SegWit, it uses the highly complex Bech32 standard, calculating polynomial checksums:
def p2wpkh(pubkey: bytes) -> str:
witprog = hash160(pubkey)
data = [0] + _convertbits(witprog, 8, 5)
combined = data + [0] * 6
polymod = _polymod(_hrp_expand("bc") + combined) ^ 1
checksum = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
return "bc1" + "".join(_CH[d] for d in data + checksum)
3. Concurrency: Multiprocessing vs. Threading
When you are scanning millions of keys or querying APIs, you hit two different types of bottlenecks: CPU bound and I/O bound. The author of this script handled both perfectly.
I/O Bound: API Lookups
To check if an address has a balance, the script pings the mempool.space API. Because waiting for an HTTP response doesn't require CPU power, the script uses a ThreadPoolExecutor. Threads are perfect here because the GIL is released during network requests.
with ThreadPoolExecutor(max_workers=12) as ex:
futs = {ex.submit(balance, t): t for t in sorted(targets)}
# ... yields results as they complete
CPU Bound: Brute-Forcing
Deriving elliptic curve points is pure, heavy math. Threads won't work because of the GIL. To utilize 100% of a multi-core CPU, the script spins up entirely separate Python processes using multiprocessing.Pool.
with mp.Pool(processes=args.workers, initializer=_worker_init, initargs=(targets, known)) as pool:
# imap_unordered streams results back immediately, rather than waiting for chunks to finish
for res in pool.imap_unordered(_check_key, target_generator, chunksize=256):
# ... progress reporting logic
Pro-tip: By using imap_unordered with a chunksize=256, the script minimizes Inter-Process Communication (IPC) overhead while keeping the terminal progress bar highly responsive.
Conclusion
This script is a masterclass in how to push Python to its limits. It elegantly handles fallback architectures (C-extensions to pure Python), implements raw cryptographic primitives from scratch, and correctly utilizes both the concurrent.futures and multiprocessing libraries depending on the exact bottleneck profile of the task.
Have you ever tried your hand at solving any of the Bitcoin Challenge puzzles? Let me know in the comments!