Skip to content

Commit 8ab307c

Browse files
committed
Merge branch 'better_errors'
2 parents 689b212 + 21ef9ed commit 8ab307c

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

bip32/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from .bip32 import BIP32
1+
from .bip32 import BIP32, PrivateDerivationError, InvalidInputError
22
from .utils import BIP32DerivationError, HARDENED_INDEX
33

44
__version__ = "0.0.8"
55

66
__all__ = [
77
"BIP32",
88
"BIP32DerivationError",
9+
"PrivateDerivationError",
10+
"InvalidInputError",
911
"HARDENED_INDEX",
1012
]

bip32/bip32.py

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@
1010
)
1111

1212

13+
class PrivateDerivationError(ValueError):
14+
"""
15+
Tried to use a derivation requiring private keys, without private keys.
16+
"""
17+
pass
18+
19+
20+
class InvalidInputError(ValueError):
21+
def __init__(self, message):
22+
self.message = message
23+
24+
1325
class BIP32:
1426
def __init__(self, chaincode, privkey=None, pubkey=None, fingerprint=None,
1527
depth=0, index=0, network="main"):
@@ -30,14 +42,21 @@ def __init__(self, chaincode, privkey=None, pubkey=None, fingerprint=None,
3042
need this for serialization.
3143
:param network: Either "main" or "test".
3244
"""
33-
assert isinstance(chaincode, bytes)
34-
assert privkey is not None or pubkey is not None
45+
if network not in ["main", "test"]:
46+
raise InvalidInputError("'network' must be one of 'main' or 'test'")
47+
if not isinstance(chaincode, bytes):
48+
raise InvalidInputError("'chaincode' must be bytes")
49+
if privkey is None and pubkey is None:
50+
raise InvalidInputError("Need at least a 'pubkey' or a 'privkey'")
3551
if privkey is not None:
36-
assert isinstance(privkey, bytes)
52+
if not isinstance(privkey, bytes):
53+
raise InvalidInputError("'privkey' must be bytes")
3754
if pubkey is not None:
38-
assert isinstance(pubkey, bytes)
55+
if not isinstance(pubkey, bytes):
56+
raise InvalidInputError("'pubkey' must be bytes")
3957
else:
4058
pubkey = _privkey_to_pubkey(privkey)
59+
4160
self.master_chaincode = chaincode
4261
self.master_privkey = privkey
4362
self.master_pubkey = pubkey
@@ -53,8 +72,12 @@ def get_extended_privkey_from_path(self, path):
5372
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
5473
:return: chaincode (bytes), privkey (bytes)
5574
"""
75+
if self.master_privkey is None:
76+
raise PrivateDerivationError
77+
5678
if isinstance(path, str):
5779
path = _deriv_path_str_to_list(path)
80+
5881
chaincode, privkey = self.master_chaincode, self.master_privkey
5982
for index in path:
6083
if index & HARDENED_INDEX:
@@ -63,6 +86,7 @@ def get_extended_privkey_from_path(self, path):
6386
else:
6487
privkey, chaincode = \
6588
_derive_unhardened_private_child(privkey, chaincode, index)
89+
6690
return chaincode, privkey
6791

6892
def get_privkey_from_path(self, path):
@@ -72,6 +96,9 @@ def get_privkey_from_path(self, path):
7296
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
7397
:return: privkey (bytes)
7498
"""
99+
if self.master_privkey is None:
100+
raise PrivateDerivationError
101+
75102
return self.get_extended_privkey_from_path(path)[1]
76103

77104
def get_extended_pubkey_from_path(self, path):
@@ -83,6 +110,10 @@ def get_extended_pubkey_from_path(self, path):
83110
"""
84111
if isinstance(path, str):
85112
path = _deriv_path_str_to_list(path)
113+
114+
if _hardened_index_in_path(path) and self.master_privkey is None:
115+
raise PrivateDerivationError
116+
86117
chaincode, key = self.master_chaincode, self.master_privkey
87118
pubkey = self.master_pubkey
88119
# We'll need the private key at some point anyway, so let's derive
@@ -102,6 +133,7 @@ def get_extended_pubkey_from_path(self, path):
102133
for index in path:
103134
pubkey, chaincode = \
104135
_derive_public_child(pubkey, chaincode, index)
136+
105137
return chaincode, pubkey
106138

107139
def get_pubkey_from_path(self, path):
@@ -120,8 +152,12 @@ def get_xpriv_from_path(self, path):
120152
m/x/x'/x notation. (e.g. m/0'/1/2'/2 or m/0H/1/2H/2).
121153
:return: The encoded extended pubkey as str.
122154
"""
155+
if self.master_privkey is None:
156+
raise PrivateDerivationError
157+
123158
if isinstance(path, str):
124159
path = _deriv_path_str_to_list(path)
160+
125161
if len(path) == 0:
126162
return self.get_master_xpriv()
127163
elif len(path) == 1:
@@ -133,6 +169,7 @@ def get_xpriv_from_path(self, path):
133169
parent_pubkey,
134170
path[-1], chaincode,
135171
self.network)
172+
136173
return base58.b58encode_check(extended_key).decode()
137174

138175
def get_xpub_from_path(self, path):
@@ -144,6 +181,10 @@ def get_xpub_from_path(self, path):
144181
"""
145182
if isinstance(path, str):
146183
path = _deriv_path_str_to_list(path)
184+
185+
if _hardened_index_in_path(path) and self.master_privkey is None:
186+
raise PrivateDerivationError
187+
147188
if len(path) == 0:
148189
return self.get_master_xpub()
149190
elif len(path) == 1:
@@ -155,10 +196,13 @@ def get_xpub_from_path(self, path):
155196
parent_pubkey,
156197
path[-1], chaincode,
157198
self.network)
199+
158200
return base58.b58encode_check(extended_key).decode()
159201

160202
def get_master_xpriv(self):
161203
"""Get the encoded extended private key of the master private key"""
204+
if self.master_privkey is None:
205+
raise PrivateDerivationError
162206
extended_key = _serialize_extended_key(self.master_privkey, self.depth,
163207
self.parent_fingerprint,
164208
self.index,
@@ -181,6 +225,9 @@ def from_xpriv(cls, xpriv):
181225
182226
:param xpriv: (str) The encoded serialized extended private key.
183227
"""
228+
if not isinstance(xpriv, str):
229+
raise InvalidInputError("'xpriv' must be a string")
230+
184231
extended_key = base58.b58decode_check(xpriv)
185232
(network, depth, fingerprint,
186233
index, chaincode, key) = _unserialize_extended_key(extended_key)
@@ -194,6 +241,9 @@ def from_xpub(cls, xpub):
194241
195242
:param xpub: (str) The encoded serialized extended public key.
196243
"""
244+
if not isinstance(xpub, str):
245+
raise InvalidInputError("'xpub' must be a string")
246+
197247
extended_key = base58.b58decode_check(xpub)
198248
(network, depth, fingerprint,
199249
index, chaincode, key) = _unserialize_extended_key(extended_key)

tests/test_bip32.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import os
33
import pytest
44

5-
from bip32 import BIP32, HARDENED_INDEX
5+
from bip32 import (
6+
BIP32, HARDENED_INDEX, PrivateDerivationError, InvalidInputError
7+
)
68

79

810
def test_vector_1():
@@ -162,3 +164,19 @@ def test_sanity_checks():
162164
# But getting from "m'" does not make sense
163165
with pytest.raises(ValueError, match="invalid format"):
164166
bip32.get_pubkey_from_path("m'")
167+
168+
# We raise if we attempt to use a privkey without privkey access
169+
bip32 = BIP32.from_xpub("xpub6C6zm7YgrLrnd7gXkyYDjQihT6F2ei9EYbNuSiDAjok7Ht56D5zbnv8WDoAJGg1RzKzK4i9U2FUwXG7TFGETFc35vpQ4sZBuYKntKMLshiq")
170+
bip32.get_master_xpub()
171+
bip32.get_pubkey_from_path("m/0/1")
172+
bip32.get_xpub_from_path("m/10000/18")
173+
with pytest.raises(PrivateDerivationError):
174+
bip32.get_master_xpriv()
175+
bip32.get_extended_privkey_from_path("m/0/1/2")
176+
bip32.get_privkey_from_path([9, 8])
177+
bip32.get_pubkey_from_path("m/0'/1")
178+
bip32.get_xpub_from_path("m/10000'/18")
179+
180+
# We can't create a BIP32 for an unknown network (to test InvalidInputError)
181+
with pytest.raises(InvalidInputError, match="'network' must be one of"):
182+
BIP32.from_seed(os.urandom(32), network="invalid_net")

0 commit comments

Comments
 (0)