Skip to content

Commit 4fc61a3

Browse files
committed
Changing methods get_XX_from_path() to handle string notation m/x/x'/x
1 parent 9a0af56 commit 4fc61a3

3 files changed

Lines changed: 75 additions & 19 deletions

File tree

bip32/bip32.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
HARDENED_INDEX, _derive_hardened_private_child,
77
_derive_unhardened_private_child, _derive_public_child,
88
_serialize_extended_key, _unserialize_extended_key,
9-
_hardened_index_in_path, _privkey_to_pubkey
9+
_hardened_index_in_path, _privkey_to_pubkey, _deriv_path_str_to_list
1010
)
1111

1212

@@ -47,12 +47,14 @@ def __init__(self, chaincode, privkey=None, pubkey=None, fingerprint=None,
4747
self.network = network
4848

4949
def get_extended_privkey_from_path(self, path):
50-
"""Get an extended privkey from a list of indexes (path).
50+
"""Get an extended privkey from a derivation path.
5151
52-
:param path: A list of integers (index of each depth).
53-
depth = len(path).
52+
:param path: A list of integers (index of each depth) or a string with
53+
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
5454
:return: chaincode (bytes), privkey (bytes)
5555
"""
56+
if isinstance(path, str):
57+
path = _deriv_path_str_to_list(path)
5658
chaincode, privkey = self.master_chaincode, self.master_privkey
5759
for index in path:
5860
if index & HARDENED_INDEX:
@@ -64,21 +66,23 @@ def get_extended_privkey_from_path(self, path):
6466
return chaincode, privkey
6567

6668
def get_privkey_from_path(self, path):
67-
"""Get a privkey from a list of indexes (path).
69+
"""Get a privkey from a derivation path.
6870
69-
:param path: A list of integers (index of each depth).
70-
depth = len(path).
71+
:param path: A list of integers (index of each depth) or a string with
72+
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
7173
:return: privkey (bytes)
7274
"""
7375
return self.get_extended_privkey_from_path(path)[1]
7476

7577
def get_extended_pubkey_from_path(self, path):
76-
"""Get an extended pubkey from a list of indexes (path).
78+
"""Get an extended pubkey from a derivation path.
7779
78-
:param path: A list of integers (index of each depth).
79-
depth = len(path).
80+
:param path: A list of integers (index of each depth) or a string with
81+
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
8082
:return: chaincode (bytes), pubkey (bytes)
8183
"""
84+
if isinstance(path, str):
85+
path = _deriv_path_str_to_list(path)
8286
chaincode, key = self.master_chaincode, self.master_privkey
8387
# We'll need the private key at some point anyway, so let's derive
8488
# everything from private keys.
@@ -102,21 +106,23 @@ def get_extended_pubkey_from_path(self, path):
102106
return chaincode, pubkey
103107

104108
def get_pubkey_from_path(self, path):
105-
"""Get a privkey from a list of indexes (path).
109+
"""Get a privkey from a derivation path.
106110
107-
:param path: A list of integers (index of each depth).
108-
depth = len(path).
111+
:param path: A list of integers (index of each depth) or a string with
112+
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
109113
:return: privkey (bytes)
110114
"""
111115
return self.get_extended_pubkey_from_path(path)[1]
112116

113117
def get_xpriv_from_path(self, path):
114-
"""Get an encoded extended privkey from a list of indexes (path).
118+
"""Get an encoded extended privkey from a derivation path.
115119
116-
:param path: A list of integers (index of each depth).
117-
depth = len(path).
120+
:param path: A list of integers (index of each depth) or a string with
121+
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
118122
:return: The encoded extended pubkey as str.
119123
"""
124+
if isinstance(path, str):
125+
path = _deriv_path_str_to_list(path)
120126
if len(path) == 0:
121127
return self.get_master_xpriv()
122128
elif len(path) == 1:
@@ -131,12 +137,14 @@ def get_xpriv_from_path(self, path):
131137
return base58.b58encode_check(extended_key).decode()
132138

133139
def get_xpub_from_path(self, path):
134-
"""Get an encoded extended pubkey from a list of indexes (path).
140+
"""Get an encoded extended pubkey from a derivation path.
135141
136-
:param path: A list of integers (index of each depth).
137-
depth = len(path).
142+
:param path: A list of integers (index of each depth) or a string with
143+
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
138144
:return: The encoded extended pubkey as str.
139145
"""
146+
if isinstance(path, str):
147+
path = _deriv_path_str_to_list(path)
140148
if len(path) == 0:
141149
return self.get_master_xpub()
142150
elif len(path) == 1:

bip32/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import coincurve
22
import hashlib
33
import hmac
4+
import re
45

56

7+
REGEX_DERIVATION_PATH = re.compile("^m(/[0-9]+['hH]?)*$")
68
HARDENED_INDEX = 0x80000000
79
ENCODING_PREFIX = {
810
"main": {
@@ -169,3 +171,25 @@ def _unserialize_extended_key(extended_key):
169171

170172
def _hardened_index_in_path(path):
171173
return len([i for i in path if i & HARDENED_INDEX]) > 0
174+
175+
176+
def _deriv_path_str_to_list(strpath):
177+
"""Converts a derivation path as string to a list of integers
178+
(index of each depth)
179+
180+
:param strpath: Derivation path as string with "m/x/x'/x" notation.
181+
(e.g. m/0'/1/2'/2 or m/0H/1/2H/2 or m/0h/1/2h/2)
182+
183+
:return: Derivation path as a list of integers (index of each depth)
184+
"""
185+
if not REGEX_DERIVATION_PATH.match(strpath):
186+
raise ValueError("invalid format")
187+
indexes = strpath.split('/')[1:]
188+
list_path = []
189+
for i in indexes:
190+
# if HARDENED
191+
if i[-1:] in ["'", "h", "H"]:
192+
list_path.append(int(i[:-1]) + HARDENED_INDEX)
193+
else:
194+
list_path.append(int(i))
195+
return list_path

tests/test_bip32.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ def test_vector_1():
1313
assert (bip32.get_xpub_from_path([HARDENED_INDEX]) ==
1414
"xpub68Gmy5EdvgibQVfPdqkBBCHxA5htiqg55crXYuXoQRKfDBFA1WEjWgP6LHhwBZeNK1VTsfTFUHCdrfp1bgwQ9xv5ski8PX9rL2dZXvgGDnw")
1515
assert (bip32.get_xpriv_from_path([HARDENED_INDEX]) == "xprv9uHRZZhk6KAJC1avXpDAp4MDc3sQKNxDiPvvkX8Br5ngLNv1TxvUxt4cV1rGL5hj6KCesnDYUhd7oWgT11eZG7XnxHrnYeSvkzY7d2bhkJ7")
16+
assert (bip32.get_xpub_from_path("m/0H") == bip32.get_xpub_from_path([HARDENED_INDEX]))
17+
assert (bip32.get_xpriv_from_path("m/0H") == bip32.get_xpriv_from_path([HARDENED_INDEX]))
1618
# m/0H/1
1719
assert (bip32.get_xpub_from_path([HARDENED_INDEX, 1]) == "xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ")
1820
assert (bip32.get_xpriv_from_path([HARDENED_INDEX, 1]) == "xprv9wTYmMFdV23N2TdNG573QoEsfRrWKQgWeibmLntzniatZvR9BmLnvSxqu53Kw1UmYPxLgboyZQaXwTCg8MSY3H2EU4pWcQDnRnrVA1xe8fs")
21+
assert (bip32.get_xpub_from_path("m/0'/1") == bip32.get_xpub_from_path([HARDENED_INDEX, 1]))
22+
assert (bip32.get_xpriv_from_path("m/0'/1") == bip32.get_xpriv_from_path([HARDENED_INDEX, 1]))
1923
# m/0H/1/2H
2024
assert (bip32.get_xpub_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2]) == "xpub6D4BDPcP2GT577Vvch3R8wDkScZWzQzMMUm3PWbmWvVJrZwQY4VUNgqFJPMM3No2dFDFGTsxxpG5uJh7n7epu4trkrX7x7DogT5Uv6fcLW5")
2125
assert (bip32.get_xpriv_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2]) == "xprv9z4pot5VBttmtdRTWfWQmoH1taj2axGVzFqSb8C9xaxKymcFzXBDptWmT7FwuEzG3ryjH4ktypQSAewRiNMjANTtpgP4mLTj34bhnZX7UiM")
26+
assert (bip32.get_xpub_from_path("m/0h/1/2h") == bip32.get_xpub_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2]))
27+
assert (bip32.get_xpriv_from_path("m/0h/1/2h") == bip32.get_xpriv_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2]))
2228
# m/0H/1/2H/2
2329
assert (bip32.get_xpub_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2, 2]) == "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV")
2430
assert (bip32.get_xpriv_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2, 2]) == "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334")
31+
assert (bip32.get_xpub_from_path("m/0'/1/2'/2") == "xpub6FHa3pjLCk84BayeJxFW2SP4XRrFd1JYnxeLeU8EqN3vDfZmbqBqaGJAyiLjTAwm6ZLRQUMv1ZACTj37sR62cfN7fe5JnJ7dh8zL4fiyLHV")
32+
assert (bip32.get_xpriv_from_path("m/0'/1/2'/2") == "xprvA2JDeKCSNNZky6uBCviVfJSKyQ1mDYahRjijr5idH2WwLsEd4Hsb2Tyh8RfQMuPh7f7RtyzTtdrbdqqsunu5Mm3wDvUAKRHSC34sJ7in334")
2533
# m/0H/1/2H/2/1000000000
2634
assert (bip32.get_xpub_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2, 2, 1000000000]) == "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy")
2735
assert (bip32.get_xpriv_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2, 2, 1000000000]) == "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76")
36+
assert (bip32.get_xpub_from_path("m/0H/1/2H/2/1000000000") == bip32.get_xpub_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2, 2, 1000000000]))
37+
assert (bip32.get_xpriv_from_path("m/0H/1/2H/2/1000000000") == bip32.get_xpriv_from_path([HARDENED_INDEX, 1, HARDENED_INDEX + 2, 2, 1000000000]))
2838

2939

3040
def test_vector_2():
@@ -36,18 +46,28 @@ def test_vector_2():
3646
# Chain m/0
3747
assert (bip32.get_xpub_from_path([0]) == "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH")
3848
assert (bip32.get_xpriv_from_path([0]) == "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt")
49+
assert (bip32.get_xpriv_from_path("m/0") == bip32.get_xpriv_from_path([0]))
50+
assert (bip32.get_xpub_from_path("m/0") == bip32.get_xpub_from_path([0]))
3951
# Chain m/0/2147483647H
4052
assert (bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647]) == "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a")
4153
assert (bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647]) == "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9")
54+
assert (bip32.get_xpub_from_path("m/0/2147483647H") == bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647]))
55+
assert (bip32.get_xpriv_from_path("m/0/2147483647H") == bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647]))
4256
# Chain m/0/2147483647H/1
4357
assert (bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647, 1]) == "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon")
4458
assert (bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647, 1]) == "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef")
59+
assert (bip32.get_xpub_from_path("m/0/2147483647H/1") == bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647, 1]))
60+
assert (bip32.get_xpriv_from_path("m/0/2147483647H/1") == bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647, 1]))
4561
# Chain m/0/2147483647H/1/2147483646H
4662
assert (bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646]) == "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL")
4763
assert (bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646]) == "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc")
64+
assert (bip32.get_xpub_from_path("m/0/2147483647H/1/2147483646H") == bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646]))
65+
assert (bip32.get_xpriv_from_path("m/0/2147483647H/1/2147483646H") == bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646]))
4866
# Chain m/0/2147483647H/1/2147483646H/2
4967
assert (bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646, 2]) == "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt")
5068
assert (bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646, 2]) == "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j")
69+
assert (bip32.get_xpub_from_path("m/0/2147483647H/1/2147483646H/2") == bip32.get_xpub_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646, 2]))
70+
assert (bip32.get_xpriv_from_path("m/0/2147483647H/1/2147483646H/2") == bip32.get_xpriv_from_path([0, HARDENED_INDEX + 2147483647, 1, HARDENED_INDEX + 2147483646, 2]))
5171

5272

5373
def test_vector_3():
@@ -56,9 +76,13 @@ def test_vector_3():
5676
# Chain m
5777
assert (bip32.get_xpub_from_path([]) == "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13")
5878
assert (bip32.get_xpriv_from_path([]) == "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6")
79+
assert (bip32.get_xpub_from_path("m") == bip32.get_xpub_from_path([]))
80+
assert (bip32.get_xpriv_from_path("m") == bip32.get_xpriv_from_path([]))
5981
# Chain m/0H
6082
assert (bip32.get_xpub_from_path([HARDENED_INDEX]) == "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y")
6183
assert (bip32.get_xpriv_from_path([HARDENED_INDEX]) == "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L")
84+
assert (bip32.get_xpub_from_path("m/0H") == bip32.get_xpub_from_path([HARDENED_INDEX]))
85+
assert (bip32.get_xpriv_from_path("m/0H") == bip32.get_xpriv_from_path([HARDENED_INDEX]))
6286

6387

6488
def test_sanity_tests():

0 commit comments

Comments
 (0)