Technical Teardown: Building a High-Performance Bitcoin Crypto-Puzzle Solver in Python

Technical Teardown: Building a High-Performance Bitcoin Crypto-Puzzle Solver in Python

Leader ●1 ●1 ●6
calendar_today ago • schedule10 min read

Technical Teardown: Building a High-Performance Bitcoin Crypto-Puzzle Solver in Python

A deep dive into the architecture, cryptography, and performance optimizations of a Python script designed to brute-force Bitcoin key spaces

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:

  1. Legacy Uncompressed (P2PKH): Starting with 1
  2. Legacy Compressed (P2PKH): Also starting with 1, but using a shorter public key representation.
  3. 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!

🔥 Join developers growing publicly
Share your knowledge, build in public, and grow your developer presence with a global community.

More Posts

Dashboard Operasional Armada Rental Mobil dengan Python + FastAPI

Masbadar - Mar 12

How Lighthouse Performance Scores Are Recorded and Calculated

ApogeeWatcherverified - Aug 18

Python for commerce students, from a former bio kid

rahuldk1105 - Oct 30

Core Web Vitals Benchmarks for Shopify Stores (2026 Data)

ApogeeWatcherverified - Sep 16

I Wrote a Script to Fix Audible's Unreadable PDF Filenames

snapsynapseverified - Apr 20
chevron_left
914 Points • 8 Badges
canada • prophetdice.tech
2Posts
5Comments
3Connections
Senior full-stack engineer and open-source contributor with 20 years in the trenches building scalab... Show more

Related Jobs

View all jobs →

Commenters (This Week)

4 comments
2 comments
1 comment

Contribute meaningful comments to climb the leaderboard and earn badges!