Skip to content

Commit af63f0f

Browse files
Fixing inconsistency between generated entropy value type and the expected HDWallet.entropy value type (#101)
* UPDATE. ignoring common IDE directories * UPDATE. ensuring entropy value stays as a string value for consistency * FIX. correctly converting bytearray into hex decoded string value * UPDATE. explicitly requiring from_entropy() method to have a string default value for passphrase parameter REFACTOR. pulling out duplicate seed creating logic into a class method ADD. adding test_is_entropy() testcase against meumonic package generated entropy value * ADD. adding a testcase for creating a HDWallet & a reward address directly from entropy value * UPDATE. only ignoring bech32.py module from coverage report * ADD. porting over MAINNET address tests from Emurgo's cardano-serialization-lib test cases to increase MAINNET specific test coverage * ADD. adding a new phony handle to ease running a single test case * ADD. explicitly specifying all phony handles * UPDATE. replacing print statements with logging statements * ADD. adding more test cases to increase test coverage * REFACTOR. Enhancing HDWallet derivation UX by supporting chained execution similar to other popular libraries * FIX. ensuring private/public root keys are passed to the child wallet * ADD. passing root_chain_code down to derived HDWallet instances * REFACTOR. pulling out frequently used hard-coded supported mnemonic language list into a constant set * UPDATE. simplifying is_mnemonic() nested loops by supporting early breakout and return statements * ADD. adding more testcases for bip32 module coverage
1 parent 195c2fb commit af63f0f

File tree

5 files changed

+244
-98
lines changed

5 files changed

+244
-98
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[run]
22
branch = True
33
omit =
4-
pycardano/crypto/*
4+
pycardano/crypto/bech32.py
55

66
[report]
77
# Regexes for lines to exclude from consideration

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
.coverage
33
cov_html
44
docs/build
5-
dist
5+
dist
6+
7+
# IDE
8+
.idea
9+
.code

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: clean clean-test clean-pyc clean-build format test help docs
1+
.PHONY: cov cov-html clean clean-test clean-pyc clean-build qa format test test-single help docs
22
.DEFAULT_GOAL := help
33

44
define BROWSER_PYSCRIPT
@@ -57,6 +57,9 @@ clean-test: ## remove test and coverage artifacts
5757
test: ## runs tests
5858
poetry run pytest -s -vv -n 4
5959

60+
test-single: ## runs tests with "single" markers
61+
poetry run pytest -s -vv -m single
62+
6063
qa: ## runs static analysis with flake8
6164
poetry run flake8 pycardano
6265

pycardano/crypto/bip32.py

Lines changed: 77 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,23 @@
1616
from mnemonic import Mnemonic
1717
from nacl import bindings
1818

19+
from pycardano.logging import logger
20+
1921
__all__ = ["BIP32ED25519PrivateKey", "BIP32ED25519PublicKey", "HDWallet"]
2022

2123

24+
SUPPORTED_MNEMONIC_LANGS = {
25+
"english",
26+
"french",
27+
"italian",
28+
"japanese",
29+
"chinese_simplified",
30+
"chinese_traditional",
31+
"korean",
32+
"spanish",
33+
}
34+
35+
2236
class BIP32ED25519PrivateKey:
2337
def __init__(self, private_key: bytes, chain_code: bytes):
2438
self.private_key = private_key
@@ -109,6 +123,9 @@ def from_seed(
109123
110124
Args:
111125
seed: Master key of 96 bytes from seed hex string.
126+
entropy: Entropy hex string, default to ``None``.
127+
passphrase: Mnemonic passphrase or password, default to ``None``.
128+
mnemonic: Mnemonic words, default to ``None``.
112129
113130
Returns:
114131
HDWallet -- Hierarchical Deterministic Wallet instance.
@@ -152,28 +169,18 @@ def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet:
152169
raise ValueError("Invalid mnemonic words.")
153170

154171
mnemonic = unicodedata.normalize("NFKD", mnemonic)
155-
passphrase = str(passphrase) if passphrase else ""
156172
entropy = Mnemonic(language="english").to_entropy(words=mnemonic)
157-
158-
seed = bytearray(
159-
hashlib.pbkdf2_hmac(
160-
"sha512",
161-
password=passphrase.encode(),
162-
salt=entropy,
163-
iterations=4096,
164-
dklen=96,
165-
)
166-
)
173+
seed = cls._generate_seed(passphrase, entropy)
167174

168175
return cls.from_seed(
169176
seed=hexlify(seed).decode(),
170177
mnemonic=mnemonic,
171-
entropy=entropy,
178+
entropy=hexlify(entropy).decode("utf-8"),
172179
passphrase=passphrase,
173180
)
174181

175182
@classmethod
176-
def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet:
183+
def from_entropy(cls, entropy: str, passphrase: str = "") -> HDWallet:
177184
"""
178185
Create master key and HDWallet from Mnemonic words.
179186
@@ -188,12 +195,20 @@ def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet:
188195
if not cls.is_entropy(entropy):
189196
raise ValueError("Invalid entropy")
190197

191-
seed = bytearray(
198+
seed = cls._generate_seed(passphrase, bytearray.fromhex(entropy))
199+
return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy)
200+
201+
@classmethod
202+
def _generate_seed(cls, passphrase: str, entropy: bytearray) -> bytearray:
203+
return bytearray(
192204
hashlib.pbkdf2_hmac(
193-
"sha512", password=passphrase, salt=entropy, iterations=4096, dklen=96
205+
"sha512",
206+
password=passphrase.encode(),
207+
salt=entropy,
208+
iterations=4096,
209+
dklen=96,
194210
)
195211
)
196-
return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy)
197212

198213
@classmethod
199214
def _tweak_bits(cls, seed: bytearray) -> bytes:
@@ -264,28 +279,26 @@ def derive_from_path(self, path: str, private: bool = True) -> HDWallet:
264279
)
265280

266281
derived_hdwallet = self._copy_hdwallet()
267-
268282
for index in path.lstrip("m/").split("/"):
269283
if index.endswith("'"):
270-
derived_hdwallet = self.derive_from_index(
271-
derived_hdwallet, int(index[:-1]), private=private, hardened=True
284+
derived_hdwallet = derived_hdwallet.derive(
285+
int(index[:-1]), private=private, hardened=True
272286
)
273287
else:
274-
derived_hdwallet = self.derive_from_index(
275-
derived_hdwallet, int(index), private=private, hardened=False
288+
derived_hdwallet = derived_hdwallet.derive(
289+
int(index), private=private, hardened=False
276290
)
277291

278292
return derived_hdwallet
279293

280-
def derive_from_index(
294+
def derive(
281295
self,
282-
parent_wallet: HDWallet,
283296
index: int,
284297
private: bool = True,
285298
hardened: bool = False,
286299
) -> HDWallet:
287300
"""
288-
Derive keys from index.
301+
Returns a new HDWallet derived from given index.
289302
290303
Args:
291304
index: Derivation index.
@@ -298,12 +311,12 @@ def derive_from_index(
298311
Examples:
299312
>>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice"
300313
>>> hdwallet = HDWallet.from_mnemonic(mnemonic_words)
301-
>>> hdwallet_l1 = hdwallet.derive_from_index(parent_wallet=hdwallet, index=1852, hardened=True)
302-
>>> hdwallet_l2 = hdwallet.derive_from_index(parent_wallet=hdwallet_l1, index=1815, hardened=True)
303-
>>> hdwallet_l3 = hdwallet.derive_from_index(parent_wallet=hdwallet_l2, index=0, hardened=True)
304-
>>> hdwallet_l4 = hdwallet.derive_from_index(parent_wallet=hdwallet_l3, index=0)
305-
>>> hdwallet_l5 = hdwallet.derive_from_index(parent_wallet=hdwallet_l4, index=0)
306-
>>> hdwallet_l5.public_key.hex()
314+
>>> hdwallet = hdwallet.derive(index=1852, hardened=True)
315+
>>> hdwallet = hdwallet.derive(index=1815, hardened=True)
316+
>>> hdwallet = hdwallet.derive(index=0, hardened=True)
317+
>>> hdwallet = hdwallet.derive(index=0)
318+
>>> hdwallet = hdwallet.derive(index=0)
319+
>>> hdwallet.public_key.hex()
307320
'73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d'
308321
"""
309322

@@ -319,19 +332,19 @@ def derive_from_index(
319332
# derive private child key
320333
if private:
321334
node = (
322-
parent_wallet._xprivate_key[:32],
323-
parent_wallet._xprivate_key[32:],
324-
parent_wallet._public_key,
325-
parent_wallet._chain_code,
326-
parent_wallet._path,
335+
self._xprivate_key[:32],
336+
self._xprivate_key[32:],
337+
self._public_key,
338+
self._chain_code,
339+
self._path,
327340
)
328341
derived_hdwallet = self._derive_private_child_key_by_index(node, index)
329342
# derive public child key
330343
else:
331344
node = (
332-
parent_wallet._public_key,
333-
parent_wallet._chain_code,
334-
parent_wallet._path,
345+
self._public_key,
346+
self._chain_code,
347+
self._path,
335348
)
336349
derived_hdwallet = self._derive_public_child_key_by_index(node, index)
337350

@@ -416,7 +429,13 @@ def _derive_private_child_key_by_index(
416429
path += "/" + str(index)
417430

418431
derived_hdwallet = HDWallet(
419-
xprivate_key=kL + kR, public_key=A, chain_code=c, path=path
432+
xprivate_key=kL + kR,
433+
public_key=A,
434+
chain_code=c,
435+
path=path,
436+
root_xprivate_key=self.root_xprivate_key,
437+
root_public_key=self.root_public_key,
438+
root_chain_code=self.root_chain_code,
420439
)
421440

422441
return derived_hdwallet
@@ -469,7 +488,14 @@ def _derive_public_child_key_by_index(
469488
# compute path
470489
path += "/" + str(index)
471490

472-
derived_hdwallet = HDWallet(public_key=A, chain_code=c, path=path)
491+
derived_hdwallet = HDWallet(
492+
public_key=A,
493+
chain_code=c,
494+
path=path,
495+
root_xprivate_key=self.root_xprivate_key,
496+
root_public_key=self.root_public_key,
497+
root_chain_code=self.root_chain_code,
498+
)
473499

474500
return derived_hdwallet
475501

@@ -510,16 +536,7 @@ def generate_mnemonic(language: str = "english", strength: int = 256) -> str:
510536
mnemonic (str): mnemonic words.
511537
"""
512538

513-
if language and language not in [
514-
"english",
515-
"french",
516-
"italian",
517-
"japanese",
518-
"chinese_simplified",
519-
"chinese_traditional",
520-
"korean",
521-
"spanish",
522-
]:
539+
if language and language not in SUPPORTED_MNEMONIC_LANGS:
523540
raise ValueError(
524541
"invalid language, use only this options english, french, "
525542
"italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages."
@@ -545,42 +562,22 @@ def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool:
545562
bool. Whether the input mnemonic words is valid.
546563
"""
547564

548-
if language and language not in [
549-
"english",
550-
"french",
551-
"italian",
552-
"japanese",
553-
"chinese_simplified",
554-
"chinese_traditional",
555-
"korean",
556-
"spanish",
557-
]:
565+
if language and language not in SUPPORTED_MNEMONIC_LANGS:
558566
raise ValueError(
559567
"invalid language, use only this options english, french, "
560568
"italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages."
561569
)
562570
try:
563571
mnemonic = unicodedata.normalize("NFKD", mnemonic)
564-
if language is None:
565-
for _language in [
566-
"english",
567-
"french",
568-
"italian",
569-
"chinese_simplified",
570-
"chinese_traditional",
571-
"japanese",
572-
"korean",
573-
"spanish",
574-
]:
575-
valid = False
576-
if Mnemonic(language=_language).check(mnemonic=mnemonic) is True:
577-
valid = True
578-
break
579-
return valid
580-
else:
572+
if language:
581573
return Mnemonic(language=language).check(mnemonic=mnemonic)
574+
575+
for _language in SUPPORTED_MNEMONIC_LANGS:
576+
if Mnemonic(language=_language).check(mnemonic=mnemonic) is True:
577+
return True
578+
return False
582579
except ValueError:
583-
print(
580+
logger.warning(
584581
"The input mnemonic words are not valid. Words should be in string format seperated by space."
585582
)
586583

@@ -599,4 +596,5 @@ def is_entropy(entropy: str) -> bool:
599596
try:
600597
return len(unhexlify(entropy)) in [16, 20, 24, 28, 32]
601598
except ValueError:
602-
print("The input entropy is not valid.")
599+
logger.warning("The input entropy is not valid.")
600+
return False

0 commit comments

Comments
 (0)