diff options
| author | Bitflash <btf01@proton.me> | 2026-08-04 10:19:38 -0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-04 10:19:38 -0300 |
| commit | a2592156207ed04671d6532cec4997fb1dab1595 (patch) | |
| tree | 6355a64910d83f0901437eebd59fbc0e04db1712 /docs/verify-derivation.py | |
| parent | f6c5b813075cae4228d241a3567e70b210504749 (diff) | |
docs: one documentation directory, not two (#137)
docs/ has held the README's images since the fair-launch release. I
added doc/ yesterday for the derivation spec without looking, and #108
then correctly used docs/ -- leaving the repository with both, one
letter apart, which is the kind of thing that quietly costs someone ten
minutes a year forever.
The mistake was mine, so the move is mine: derivation.md and
verify-derivation.py go to docs/, and the two README links follow.
No content changes. The verifier still reproduces the wallet from its
new path, which is the only thing here that can actually break.
Diffstat (limited to 'docs/verify-derivation.py')
| -rw-r--r-- | docs/verify-derivation.py | 159 |
1 files changed, 159 insertions, 0 deletions
diff --git a/docs/verify-derivation.py b/docs/verify-derivation.py new file mode 100644 index 0000000..ba8e883 --- /dev/null +++ b/docs/verify-derivation.py | |||
| @@ -0,0 +1,159 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | """Check doc/derivation.md against the addresses the wallet really produces. | ||
| 3 | |||
| 4 | This implements the document and nothing else: BIP-39 to seed, BIP-32 to child | ||
| 5 | keys, uncompressed secp256k1 public keys, and Base58Check with version byte 25. | ||
| 6 | It shares no code with Bitflash and needs nothing outside the standard library, | ||
| 7 | so agreement between the two is evidence that the document is enough to recover | ||
| 8 | a wallet without this project's software. | ||
| 9 | |||
| 10 | python doc/verify-derivation.py | ||
| 11 | |||
| 12 | Exits non-zero if any address disagrees. | ||
| 13 | """ | ||
| 14 | |||
| 15 | import hashlib | ||
| 16 | import hmac | ||
| 17 | import sys | ||
| 18 | |||
| 19 | MNEMONIC = ("abandon abandon abandon abandon abandon abandon " | ||
| 20 | "abandon abandon abandon abandon abandon about") | ||
| 21 | COIN_TYPE = 4346950 # 0x425446, the ASCII bytes "BTF" | ||
| 22 | ADDRESS_VERSION = 25 # 0x19, so addresses start with "B" | ||
| 23 | HARDENED = 0x80000000 | ||
| 24 | |||
| 25 | LEGACY = [ | ||
| 26 | "BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy", | ||
| 27 | "B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ", | ||
| 28 | "BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin", | ||
| 29 | "BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD", | ||
| 30 | ] | ||
| 31 | |||
| 32 | BIP44_EXTERNAL = [ | ||
| 33 | "BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb", | ||
| 34 | "BACY8pcuCapNQr897eRHroNrgN38GSsEzV", | ||
| 35 | "BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa", | ||
| 36 | "BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ", | ||
| 37 | ] | ||
| 38 | |||
| 39 | # ---- secp256k1, plain and slow on purpose: it should be readable ------------ | ||
| 40 | |||
| 41 | P = 2**256 - 2**32 - 977 | ||
| 42 | N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 | ||
| 43 | G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, | ||
| 44 | 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) | ||
| 45 | |||
| 46 | |||
| 47 | def point_add(p, q): | ||
| 48 | if p is None: | ||
| 49 | return q | ||
| 50 | if q is None: | ||
| 51 | return p | ||
| 52 | if p[0] == q[0] and (p[1] + q[1]) % P == 0: | ||
| 53 | return None | ||
| 54 | if p == q: | ||
| 55 | lam = 3 * p[0] * p[0] * pow(2 * p[1], P - 2, P) % P | ||
| 56 | else: | ||
| 57 | lam = (q[1] - p[1]) * pow(q[0] - p[0], P - 2, P) % P | ||
| 58 | x = (lam * lam - p[0] - q[0]) % P | ||
| 59 | return (x, (lam * (p[0] - x) - p[1]) % P) | ||
| 60 | |||
| 61 | |||
| 62 | def point_mul(k, p=G): | ||
| 63 | result = None | ||
| 64 | while k: | ||
| 65 | if k & 1: | ||
| 66 | result = point_add(result, p) | ||
| 67 | p = point_add(p, p) | ||
| 68 | k >>= 1 | ||
| 69 | return result | ||
| 70 | |||
| 71 | |||
| 72 | def pub_uncompressed(point): | ||
| 73 | return b"\x04" + point[0].to_bytes(32, "big") + point[1].to_bytes(32, "big") | ||
| 74 | |||
| 75 | |||
| 76 | def pub_compressed(point): | ||
| 77 | return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big") | ||
| 78 | |||
| 79 | |||
| 80 | # ---- encoding -------------------------------------------------------------- | ||
| 81 | |||
| 82 | def base58check(payload): | ||
| 83 | alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" | ||
| 84 | checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4] | ||
| 85 | data = payload + checksum | ||
| 86 | number = int.from_bytes(data, "big") | ||
| 87 | out = "" | ||
| 88 | while number: | ||
| 89 | number, rem = divmod(number, 58) | ||
| 90 | out = alphabet[rem] + out | ||
| 91 | leading_zeros = len(data) - len(data.lstrip(b"\x00")) | ||
| 92 | return "1" * leading_zeros + out | ||
| 93 | |||
| 94 | |||
| 95 | def address(privkey): | ||
| 96 | pub = pub_uncompressed(point_mul(privkey)) | ||
| 97 | h160 = hashlib.new("ripemd160", hashlib.sha256(pub).digest()).digest() | ||
| 98 | return base58check(bytes([ADDRESS_VERSION]) + h160) | ||
| 99 | |||
| 100 | |||
| 101 | # ---- BIP-39 and BIP-32 ----------------------------------------------------- | ||
| 102 | |||
| 103 | def master_from_mnemonic(mnemonic, passphrase=""): | ||
| 104 | seed = hashlib.pbkdf2_hmac("sha512", mnemonic.encode("utf-8"), | ||
| 105 | ("mnemonic" + passphrase).encode("utf-8"), | ||
| 106 | 2048, 64) | ||
| 107 | i = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest() | ||
| 108 | return int.from_bytes(i[:32], "big"), i[32:] | ||
| 109 | |||
| 110 | |||
| 111 | def ckd_priv(key, chain, index): | ||
| 112 | if index >= HARDENED: | ||
| 113 | data = b"\x00" + key.to_bytes(32, "big") | ||
| 114 | else: | ||
| 115 | data = pub_compressed(point_mul(key)) | ||
| 116 | i = hmac.new(chain, data + index.to_bytes(4, "big"), hashlib.sha512).digest() | ||
| 117 | return (int.from_bytes(i[:32], "big") + key) % N, i[32:] | ||
| 118 | |||
| 119 | |||
| 120 | def derive(path): | ||
| 121 | key, chain = master_from_mnemonic(MNEMONIC) | ||
| 122 | for index in path: | ||
| 123 | key, chain = ckd_priv(key, chain, index) | ||
| 124 | return key | ||
| 125 | |||
| 126 | |||
| 127 | # ---- the check ------------------------------------------------------------- | ||
| 128 | |||
| 129 | def main(): | ||
| 130 | failures = 0 | ||
| 131 | |||
| 132 | print("legacy m/i'") | ||
| 133 | for i, expected in enumerate(LEGACY): | ||
| 134 | got = address(derive([i + HARDENED])) | ||
| 135 | ok = got == expected | ||
| 136 | failures += 0 if ok else 1 | ||
| 137 | print(" m/%d' %s %s" % (i, got, "ok" if ok else | ||
| 138 | "MISMATCH, wallet says " + expected)) | ||
| 139 | |||
| 140 | print() | ||
| 141 | print("bip-44 m/44'/%d'/0'/0/i" % COIN_TYPE) | ||
| 142 | for i, expected in enumerate(BIP44_EXTERNAL): | ||
| 143 | got = address(derive([44 + HARDENED, COIN_TYPE + HARDENED, | ||
| 144 | 0 + HARDENED, 0, i])) | ||
| 145 | ok = got == expected | ||
| 146 | failures += 0 if ok else 1 | ||
| 147 | print(" .../%d %s %s" % (i, got, "ok" if ok else | ||
| 148 | "MISMATCH, wallet says " + expected)) | ||
| 149 | |||
| 150 | print() | ||
| 151 | if failures: | ||
| 152 | print("%d address(es) disagree -- doc/derivation.md is wrong" % failures) | ||
| 153 | return 1 | ||
| 154 | print("all addresses match: the document reproduces the wallet") | ||
| 155 | return 0 | ||
| 156 | |||
| 157 | |||
| 158 | if __name__ == "__main__": | ||
| 159 | sys.exit(main()) | ||