
Educational study of Bitcoin key entropy: weak-RNG wallet enumeration (Milk Sad / CVE-2023-39910 class) vs. the infeasibility of brute-forcing a good key.
| 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_entropy enumerates a self-contained synthetic space. Its
weak_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_search is 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.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 --help
weak_entropy: weak entropy is enumerableThe 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 1000
Rows 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.
blind_search: you can't brute-force a good keyA 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 addrs
The 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:
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.
ecdsa with coincurve (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, from an
exact closed form for the seed32 digits plus a measured mean address length.MIT — see LICENSE. Usage terms and responsible-disclosure guidance: DISCLAIMER.md. Reporting: SECURITY.md.