extendedkey.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2026-present The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Base classes for creating dynamic xprvs and xpubs. Only basic
6 functionality of BIP32 is provided here. These classes work over
7 the ECKey and ECPubKey classes in the key.py."""
8
9 import hashlib
10 import hmac
11 import unittest
12
13 from test_framework.address import byte_to_base58
14 from test_framework.crypto import secp256k1
15 from test_framework.key import (
16 ECKey,
17 ECPubKey,
18 generate_privkey,
19 ORDER,
20 )
21 from test_framework.script import hash160
22
23 BIP32_HARDENED = 0x80000000
24
25 def get_version(private=False, mainnet=False):
26 if mainnet:
27 if private:
28 return bytes.fromhex("0488ADE4")
29 return bytes.fromhex("0488B21E")
30
31 if private:
32 return bytes.fromhex("04358394")
33 return bytes.fromhex("043587CF")
34
35 def hardened(index):
36 assert 0 <= index < BIP32_HARDENED
37 return index | BIP32_HARDENED
38
39 def derive_path(key, path, path_idx_parser):
40 if path in ("", "m"):
41 return key
42 parts = path.split("/")
43 if parts[0] == "m":
44 parts = parts[1:]
45 for part in parts:
46 index = path_idx_parser(part)
47 key = key._derive(index)
48
49 return key
50
51 class ExtendedPrivateKey:
52 def __init__(self, key, chaincode, depth=0, parent_fingerprint_bytes=b"\x00\x00\x00\x00", child_num=0):
53 self.key = key
54 self.chaincode = chaincode
55 self.depth = depth
56 self.parent_fingerprint_bytes = parent_fingerprint_bytes
57 self.child_num = child_num
58
59 @classmethod
60 def from_seed(cls, seed):
61 I = hmac.new(b"Bitcoin seed", seed, hashlib.sha512).digest()
62
63 secret = I[:32]
64 chaincode = I[32:]
65
66 secret_int = int.from_bytes(secret, "big")
67 if secret_int == 0 or secret_int >= ORDER:
68 raise ValueError("Invalid master key")
69
70 key = ECKey()
71 key.set(secret, compressed=True)
72
73 return cls(key, chaincode)
74
75 @classmethod
76 def generate(cls):
77 return cls.from_seed(generate_privkey())
78
79 def _fingerprint(self):
80 return hash160(self.key.get_pubkey().get_bytes())[:4]
81
82 def _derive(self, index):
83 if index >= BIP32_HARDENED:
84 data = (b"\x00" + self.key.get_bytes() + index.to_bytes(4, "big"))
85 else:
86 data = (self.key.get_pubkey().get_bytes() + index.to_bytes(4, "big"))
87
88 I = hmac.new(self.chaincode, data, hashlib.sha512).digest()
89 IL = I[:32]
90 IR = I[32:]
91
92 IL_int = int.from_bytes(IL, "big")
93 child_secret = (IL_int + int.from_bytes(self.key.get_bytes(), "big")) % ORDER
94 # Per BIP32, if IL >= n or the child key is 0, the key is invalid. This is
95 # astronomically unlikely (~1 in 2^127), so reject rather than retrying the next index.
96 if IL_int >= ORDER or child_secret == 0:
97 raise ValueError("Invalid BIP32 child key")
98
99 child = ECKey()
100 child.set(child_secret.to_bytes(32, 'big'), compressed=True)
101
102 return ExtendedPrivateKey(child, IR, self.depth + 1, self._fingerprint(), index)
103
104 def _serialize(self):
105 return (bytes([self.depth]) + self.parent_fingerprint_bytes + self.child_num.to_bytes(4, "big") + self.chaincode + b"\x00" + self.key.get_bytes())
106
107 def pubkey(self):
108 return ExtendedPublicKey(self.key.get_pubkey(), self.chaincode, self.depth, self.parent_fingerprint_bytes, self.child_num)
109
110 def derive_path(self, path):
111 def path_idx_parser(part):
112 if part.endswith(("h", "'")):
113 return hardened(int(part[:-1]))
114 return int(part)
115
116 return derive_path(self, path, path_idx_parser)
117
118 def to_string(self, mainnet=False):
119 return byte_to_base58(self._serialize(), get_version(private=True, mainnet=mainnet))
120
121 class ExtendedPublicKey:
122 def __init__(self, pubkey, chaincode, depth, parent_fingerprint_bytes, child_num):
123 self.pubkey = pubkey
124 self.chaincode = chaincode
125 self.depth = depth
126 self.parent_fingerprint_bytes = parent_fingerprint_bytes
127 self.child_num = child_num
128
129 def _fingerprint(self):
130 return hash160(self.pubkey.get_bytes())[:4]
131
132 def _derive(self, index):
133 if index >= BIP32_HARDENED:
134 raise ValueError("Cannot derive hardened child from xpub")
135
136 data = (self.pubkey.get_bytes() + index.to_bytes(4, "big"))
137 I = hmac.new(self.chaincode, data, hashlib.sha512).digest()
138 IL = I[:32]
139 IR = I[32:]
140
141 IL_int = int.from_bytes(IL, "big")
142 child_point = IL_int * secp256k1.G + self.pubkey.p
143 # Per BIP32, if IL >= n or the resulting point is infinity, the key is invalid.
144 if IL_int >= ORDER or child_point.infinity:
145 raise ValueError("Invalid BIP32 child key")
146
147 child_pubkey = ECPubKey()
148 child_pubkey.set(child_point.to_bytes_compressed())
149
150 return ExtendedPublicKey(child_pubkey, IR, self.depth + 1, self._fingerprint(), index)
151
152 def _serialize(self):
153 return (bytes([self.depth]) + self.parent_fingerprint_bytes + self.child_num.to_bytes(4, "big") + self.chaincode + self.pubkey.get_bytes())
154
155 def derive_path(self, path):
156 return derive_path(self, path, lambda x: int(x))
157
158 def to_string(self, mainnet=False):
159 return byte_to_base58(self._serialize(), get_version(private=False, mainnet=mainnet))
160
161 class TestFrameworkExtendedKey(unittest.TestCase):
162 def test_bip32_vectors(self):
163 vectors = [
164 [
165 "000102030405060708090a0b0c0d0e0f",
166 [
167 ["m", "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"],
168 ["m/0h", "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7", "xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw"],
169 ["m/0h/1", "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs", "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ"],
170 ["m/0h/1/2h", "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM", "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5"],
171 ["m/0h/1/2h/2", "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334", "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV"],
172 ["m/0h/1/2h/2/1000000000", "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76", "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy"],
173 ]
174 ],
175 [
176 "fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542",
177 [
178 ["m", "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U", "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB"],
179 ["m/0", "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt", "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH"],
180 ["m/0/2147483647h", "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9", "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a"],
181 ["m/0/2147483647h/1", "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef", "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon"],
182 ["m/0/2147483647h/1/2147483646h", "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc", "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL"],
183 ["m/0/2147483647h/1/2147483646h/2", "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j", "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt"]
184 ]
185 ]
186 ]
187
188 for vector in vectors:
189 seed = bytes.fromhex(vector[0])
190 xprv = ExtendedPrivateKey.from_seed(seed)
191 for seed_vector in vector[1]:
192 path = seed_vector[0]
193 derivedxprv = xprv.derive_path(path)
194 self.assertEqual(derivedxprv.to_string(mainnet=True), seed_vector[1])
195 self.assertEqual(derivedxprv.pubkey().to_string(mainnet=True), seed_vector[2])
196