upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/docs/verify-derivation.py
diff options
context:
space:
mode:
authorbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
committerbitflash-coin <bookofshamers@outlook.com>2026-08-04 06:34:32 -0700
commit5ce650382352c1503f40abf08129ada69402af55 (patch)
tree6a18198901f1e2c4933f1ac53c9365eec512bf63 /docs/verify-derivation.py
parentf34113291da7e5e3dc7d2468d4f9c212c05c584b (diff)
public commit
Diffstat (limited to 'docs/verify-derivation.py')
-rw-r--r--docs/verify-derivation.py159
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
4This implements the document and nothing else: BIP-39 to seed, BIP-32 to child
5keys, uncompressed secp256k1 public keys, and Base58Check with version byte 25.
6It shares no code with Bitflash and needs nothing outside the standard library,
7so agreement between the two is evidence that the document is enough to recover
8a wallet without this project's software.
9
10 python doc/verify-derivation.py
11
12Exits non-zero if any address disagrees.
13"""
14
15import hashlib
16import hmac
17import sys
18
19MNEMONIC = ("abandon abandon abandon abandon abandon abandon "
20 "abandon abandon abandon abandon abandon about")
21COIN_TYPE = 4346950 # 0x425446, the ASCII bytes "BTF"
22ADDRESS_VERSION = 25 # 0x19, so addresses start with "B"
23HARDENED = 0x80000000
24
25LEGACY = [
26 "BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy",
27 "B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ",
28 "BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin",
29 "BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD",
30]
31
32BIP44_EXTERNAL = [
33 "BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb",
34 "BACY8pcuCapNQr897eRHroNrgN38GSsEzV",
35 "BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa",
36 "BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ",
37]
38
39# ---- secp256k1, plain and slow on purpose: it should be readable ------------
40
41P = 2**256 - 2**32 - 977
42N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
43G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
44 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
45
46
47def 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
62def 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
72def pub_uncompressed(point):
73 return b"\x04" + point[0].to_bytes(32, "big") + point[1].to_bytes(32, "big")
74
75
76def pub_compressed(point):
77 return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big")
78
79
80# ---- encoding --------------------------------------------------------------
81
82def 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
95def 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
103def 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
111def 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
120def 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
129def 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
158if __name__ == "__main__":
159 sys.exit(main())