An educational study of where Bitcoin key security actually comes from — and where it doesn't. Built around real BIP39/BIP32 derivation, with two experiments that sit on opposite ends of the same idea:
| Experiment | Search space | Feasible? | What it teaches |
|---|---|---|---|
weak_entropy — enumerate a weak-RNG wallet space |
2³² (a bad RNG) | Yes, in days | A wallet is only as strong as the entropy behind it. |
blind_search — brute-force a good key space |
2¹⁶⁰ (a good RNG) | No, ever | With real entropy, the key space is physically unsearchable. |
The whole point is the contrast: keys are stolen by attacking bad randomness, never by out-computing good randomness. Both experiments derive the same kind of real mainnet addresses, so the only variable is entropy.
This repository is for defensive education and security research: understanding why weak-RNG wallet bugs (Milk Sad / CVE-2023-39910, the 2018 Trust Wallet extension bug, and their kin) were catastrophic, and why brute-forcing a properly-generated key is not.
weak_entropyenumerates a self-contained synthetic space. Itsweak_entropy()is a deliberate model of the vulnerability class — a 32-bit PRNG feeding BIP39 — not a byte-exact clone of any real wallet's RNG. The addresses it produces do not correspond to anyone's real wallet.blind_searchis a read-only balance lookup against your own node. It derives no keys and can spend nothing; its headline mode measures the futility of blind search.- This project intentionally does not provide, and will not be extended to provide, a pipeline that targets real victims' funds (e.g. reproducing a real wallet's RNG, or joining funded-address hits back to recovered keys). That is the line between studying the vulnerability and weaponizing it.
Use it on wallets you control, on testnet, or as a sandbox. Don't point it at other people's money.
For education and authorized security research only. Use it solely on keys, wallets, and systems you own or have explicit written permission to test. Unauthorized access to others' wallets or systems, and theft of digital assets, are illegal under laws such as the US CFAA (18 U.S.C. § 1030), the UK Computer Misuse Act 1990, and EU Directive 2013/40/EU — regardless of the technical means. Provided "as is", without warranty; the authors accept no liability for misuse. This is not legal advice. Full terms and responsible-disclosure guidance: DISCLAIMER.md.
A wallet's security is bounded by the entropy that went into its seed, not by the strength of SHA-256 / PBKDF2 / secp256k1 downstream.
BIP39 turns entropy into a mnemonic, PBKDF2 stretches it into a seed, and BIP32 derives a tree of keys. All of that is strong. But none of it adds entropy — if the seed was drawn from a small set, every address the wallet can ever produce is enumerable, and no amount of hashing hides that.
bitcoin-security/
├── bitcoin_security/ # the package
│ ├── derivation.py # shared core: BIP39 + BIP32 + P2PKH/WIF
│ ├── data/english.txt # BIP39 English wordlist (2048 words)
│ ├── weak_entropy/ # Experiment 1 — enumerable weak space
│ │ ├── enumerate.py # recovery demo + full-space dump
│ │ └── README.md
│ └── blind_search/ # Experiment 2 — futile brute-force search
│ ├── scan.py # UTXO-set balance scanner + odds
│ └── README.md
├── tests/ # offline test suites (no node/network)
│ ├── test_blind_search_addresses.py
│ └── test_blind_search_pipeline.py
├── docs/FINDINGS.md # benchmarks + exact full-space size derivation
├── pyproject.toml
├── requirements.txt
└── LICENSE
The tree reflects the idea: one shared derivation core, two experiments that
use it to reach opposite conclusions about the same address space.
pip install -r requirements.txt # runtime deps
# optional — installs the package + `bsec-enumerate` / `bsec-scan` commands:
pip install -e .coincurve (native libsecp256k1) makes derivation ~5× faster but is optional —
derivation.py falls back to pure-Python ecdsa with identical output. Nothing
here touches the network except the opt-in --check-balance flag and
blind_search (which needs a local Bitcoin node).
Everything runs as a module from the repo root; no install required:
python3 -m bitcoin_security.weak_entropy --help
python3 -m bitcoin_security.blind_search --helpThe wallet's "random" 16 bytes of BIP39 entropy actually come from a 32-bit seed. The full derivation is real and verified against spec vectors.
seed32 ──MT19937──▶ 16-byte entropy ──BIP39──▶ mnemonic ──PBKDF2(2048)──▶ 64-byte seed
│
address ◀──hash160──── pubkey ◀──BIP32 m/44'/0'/0'/0/0
# Recovery: derive a victim wallet from a weak seed, then recover its full
# mnemonic + private key knowing only the public address.
python3 -m bitcoin_security.weak_entropy
# Expand one seed into every form.
python3 -m bitcoin_security.weak_entropy --show 3571
# Materialize the whole 2**32 space to sharded, resumable files.
python3 -m bitcoin_security.weak_entropy --dump-all --out-dir space_dump --max-file-mb 1000Rows are minimal (address <TAB> seed32) because every other form is a
deterministic function of seed32, regenerated on demand with --show. Dumps
are resumable at shard granularity (verified to reproduce contiguous,
gap-free, duplicate-free coverage). Full flag reference in
weak_entropy/README.md.
Scale warning. The full 2³² dump is ~196 GB (minimal) or ~1.05 TB (
--full) and takes days even with the native backend — seedocs/FINDINGS.md. You almost never need the whole thing; dump a bounded slice for inspection.
A real, working balance scanner over a local (pruning-OK) node's UTXO set via
scantxoutset. Its purpose is the null result: search the address space
blind, measure that you find nothing, and print the arithmetic showing you always
will.
python3 -m bitcoin_security.blind_search --odds-only # just the math, no node
python3 -m bitcoin_security.blind_search --random 200000 --odds # search 200k random addrs, then the math
python3 -m bitcoin_security.blind_search --control # verify the pipeline finds known-funded addrsThe result: the space is 2¹⁶⁰ ≈ 1.46×10⁴⁸; ~55M addresses hold a balance; so a
random address is funded with probability ~3.8×10⁻⁴¹. Even at a billion
addresses/second you'd wait ~8×10²³ years — ~61 trillion times the age of the
universe. Not a hardware problem: by Landauer's limit, merely counting to 2¹⁶⁰
exceeds the sun's remaining energy output. Node setup in
blind_search/README.md.
Both experiments derive the same kind of address. The only difference is the size of the set the seed was drawn from:
- Good entropy → 2¹⁶⁰, unsearchable by physics. (Experiment 2)
- Weak entropy → 2³², exhaustible in days on a laptop. (Experiment 1)
That gap — 128 bits — is the entire ballgame. Every real theft in this class lived in it.
python3 tests/test_blind_search_addresses.py # BIP-173/350/380 address vectors
python3 tests/test_blind_search_pipeline.py # scan pipeline, RPC stubbed
python3 -m bitcoin_security.derivation # derivation self-test (spec vectors)Full detail, methods, and machine specs in docs/FINDINGS.md.
- Correctness: derivation verified against BIP39/BIP32/BIP-173/350/380 spec vectors; the scanner ships with offline test suites and a mandatory known-funded control address per run.
- Performance: replacing pure-Python
ecdsawithcoincurve(libsecp256k1) cut per-wallet derivation from 3.02 ms → 0.591 ms (~5.1×) on an Apple M3 Max; the remaining floor is PBKDF2-HMAC-SHA512 ×2048 at 0.463 ms/wallet. - Full-space size (exact): 196.3 GB minimal / ~1.05 TB
--full, from an exact closed form for theseed32digits plus a measured mean address length. - Full-space time: ~29 CPU-days single-core; ~2–3 days across the M3 Max's performance cores; hard PBKDF2 floor ~1.6 days. "Hours" would need a GPU.
- CVE-2023-39910 — "Milk Sad", libbitcoin-explorer
bx seed(32-bit MT19937 seeded from time). Real funds stolen. https://milksad.info/ - Trust Wallet browser-extension weak entropy (2018).
- BIPs: 39 (mnemonics), 32 (HD wallets), 44 (paths), 173/350 (bech32/bech32m), 380 (descriptors).
MIT — see LICENSE. Usage terms and responsible-disclosure guidance: DISCLAIMER.md. Reporting: SECURITY.md.