1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
|
#!/usr/bin/env python3
"""Check doc/derivation.md against the addresses the wallet really produces.
This implements the document and nothing else: BIP-39 to seed, BIP-32 to child
keys, uncompressed secp256k1 public keys, and Base58Check with version byte 25.
It shares no code with Bitflash and needs nothing outside the standard library,
so agreement between the two is evidence that the document is enough to recover
a wallet without this project's software.
python doc/verify-derivation.py
Exits non-zero if any address disagrees.
"""
import hashlib
import hmac
import sys
MNEMONIC = ("abandon abandon abandon abandon abandon abandon "
"abandon abandon abandon abandon abandon about")
COIN_TYPE = 4346950 # 0x425446, the ASCII bytes "BTF"
ADDRESS_VERSION = 25 # 0x19, so addresses start with "B"
HARDENED = 0x80000000
LEGACY = [
"BHJNmVvkVv59fgn7gmgP78BtciQi2HY8Hy",
"B5qh9opG8sH8Mv9k2xzHoTeSUd9BkT6XEZ",
"BCNH8uGyvkiz3uXjhA9iiF27PWAaTStNin",
"BC1QNaXuTPGDgLZy53PghZMHEmZcp5GLgD",
]
BIP44_EXTERNAL = [
"BF1ukigB1afmqxH64gJymPwUqFBrRqWbrb",
"BACY8pcuCapNQr897eRHroNrgN38GSsEzV",
"BMzkMN2k1phNBqy8RPb4Pv1i2QpwsWkKPa",
"BNPZ8U6FHXr9Ererk8psyM2RqEVtF6sfBQ",
]
# ---- secp256k1, plain and slow on purpose: it should be readable ------------
P = 2**256 - 2**32 - 977
N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798,
0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8)
def point_add(p, q):
if p is None:
return q
if q is None:
return p
if p[0] == q[0] and (p[1] + q[1]) % P == 0:
return None
if p == q:
lam = 3 * p[0] * p[0] * pow(2 * p[1], P - 2, P) % P
else:
lam = (q[1] - p[1]) * pow(q[0] - p[0], P - 2, P) % P
x = (lam * lam - p[0] - q[0]) % P
return (x, (lam * (p[0] - x) - p[1]) % P)
def point_mul(k, p=G):
result = None
while k:
if k & 1:
result = point_add(result, p)
p = point_add(p, p)
k >>= 1
return result
def pub_uncompressed(point):
return b"\x04" + point[0].to_bytes(32, "big") + point[1].to_bytes(32, "big")
def pub_compressed(point):
return bytes([2 + (point[1] & 1)]) + point[0].to_bytes(32, "big")
# ---- encoding --------------------------------------------------------------
def base58check(payload):
alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
checksum = hashlib.sha256(hashlib.sha256(payload).digest()).digest()[:4]
data = payload + checksum
number = int.from_bytes(data, "big")
out = ""
while number:
number, rem = divmod(number, 58)
out = alphabet[rem] + out
leading_zeros = len(data) - len(data.lstrip(b"\x00"))
return "1" * leading_zeros + out
def address(privkey):
pub = pub_uncompressed(point_mul(privkey))
h160 = hashlib.new("ripemd160", hashlib.sha256(pub).digest()).digest()
return base58check(bytes([ADDRESS_VERSION]) + h160)
# ---- BIP-39 and BIP-32 -----------------------------------------------------
def master_from_mnemonic(mnemonic, passphrase=""):
seed = hashlib.pbkdf2_hmac("sha512", mnemonic.encode("utf-8"),
("mnemonic" + passphrase).encode("utf-8"),
2048, 64)
i = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
return int.from_bytes(i[:32], "big"), i[32:]
def ckd_priv(key, chain, index):
if index >= HARDENED:
data = b"\x00" + key.to_bytes(32, "big")
else:
data = pub_compressed(point_mul(key))
i = hmac.new(chain, data + index.to_bytes(4, "big"), hashlib.sha512).digest()
return (int.from_bytes(i[:32], "big") + key) % N, i[32:]
def derive(path):
key, chain = master_from_mnemonic(MNEMONIC)
for index in path:
key, chain = ckd_priv(key, chain, index)
return key
# ---- the check -------------------------------------------------------------
def main():
failures = 0
print("legacy m/i'")
for i, expected in enumerate(LEGACY):
got = address(derive([i + HARDENED]))
ok = got == expected
failures += 0 if ok else 1
print(" m/%d' %s %s" % (i, got, "ok" if ok else
"MISMATCH, wallet says " + expected))
print()
print("bip-44 m/44'/%d'/0'/0/i" % COIN_TYPE)
for i, expected in enumerate(BIP44_EXTERNAL):
got = address(derive([44 + HARDENED, COIN_TYPE + HARDENED,
0 + HARDENED, 0, i]))
ok = got == expected
failures += 0 if ok else 1
print(" .../%d %s %s" % (i, got, "ok" if ok else
"MISMATCH, wallet says " + expected))
print()
if failures:
print("%d address(es) disagree -- doc/derivation.md is wrong" % failures)
return 1
print("all addresses match: the document reproduces the wallet")
return 0
if __name__ == "__main__":
sys.exit(main())
|