From 682cc9bc67ea0ecef90d4db4e24c3aade94923a5 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Sun, 16 Oct 2022 23:47:21 -0700 Subject: [PATCH 01/17] UPDATE. ignoring common IDE directories --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 893c3a7f..88a8b689 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ .coverage cov_html docs/build -dist \ No newline at end of file +dist + +# IDE +.idea +.code \ No newline at end of file From 5ecc14257d702ec03724d60b6343f9d445f79793 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Sun, 16 Oct 2022 23:50:23 -0700 Subject: [PATCH 02/17] UPDATE. ensuring entropy value stays as a string value for consistency --- pycardano/crypto/bip32.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 01a8821e..dfcbe3d0 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -109,6 +109,9 @@ def from_seed( Args: seed: Master key of 96 bytes from seed hex string. + entropy: Entropy hex string, default to ``None``. + passphrase: Mnemonic passphrase or password, default to ``None``. + mnemonic: Mnemonic words, default to ``None``. Returns: HDWallet -- Hierarchical Deterministic Wallet instance. @@ -168,7 +171,7 @@ def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet: return cls.from_seed( seed=hexlify(seed).decode(), mnemonic=mnemonic, - entropy=entropy, + entropy=unhexlify(entropy).decode("utf-8"), passphrase=passphrase, ) From 692682ee1310055e1ae95d0323d80b1432270699 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 00:29:28 -0700 Subject: [PATCH 03/17] FIX. correctly converting bytearray into hex decoded string value --- pycardano/crypto/bip32.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index dfcbe3d0..375fdd65 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -171,7 +171,7 @@ def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet: return cls.from_seed( seed=hexlify(seed).decode(), mnemonic=mnemonic, - entropy=unhexlify(entropy).decode("utf-8"), + entropy=hexlify(entropy).decode("utf-8"), passphrase=passphrase, ) From d6838b3304a534191f4eb0417ab092f586973efb Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 00:53:11 -0700 Subject: [PATCH 04/17] 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 --- pycardano/crypto/bip32.py | 28 +++++++++++++--------------- test/pycardano/backend/test_bip32.py | 8 ++++++++ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 375fdd65..d64d1d49 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -155,18 +155,8 @@ def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet: raise ValueError("Invalid mnemonic words.") mnemonic = unicodedata.normalize("NFKD", mnemonic) - passphrase = str(passphrase) if passphrase else "" entropy = Mnemonic(language="english").to_entropy(words=mnemonic) - - seed = bytearray( - hashlib.pbkdf2_hmac( - "sha512", - password=passphrase.encode(), - salt=entropy, - iterations=4096, - dklen=96, - ) - ) + seed = cls._generate_seed(passphrase, entropy) return cls.from_seed( seed=hexlify(seed).decode(), @@ -176,7 +166,7 @@ def from_mnemonic(cls, mnemonic: str, passphrase: str = "") -> HDWallet: ) @classmethod - def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet: + def from_entropy(cls, entropy: str, passphrase: str = "") -> HDWallet: """ Create master key and HDWallet from Mnemonic words. @@ -191,12 +181,20 @@ def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet: if not cls.is_entropy(entropy): raise ValueError("Invalid entropy") - seed = bytearray( + seed = cls._generate_seed(passphrase, bytearray.fromhex(entropy)) + return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy) + + @classmethod + def _generate_seed(cls, passphrase: str, entropy: bytearray) -> bytearray: + return bytearray( hashlib.pbkdf2_hmac( - "sha512", password=passphrase, salt=entropy, iterations=4096, dklen=96 + "sha512", + password=passphrase.encode(), + salt=entropy, + iterations=4096, + dklen=96, ) ) - return cls.from_seed(seed=hexlify(seed).decode(), entropy=entropy) @classmethod def _tweak_bits(cls, seed: bytearray) -> bytes: diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index 21097ed0..d012cb77 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -1,3 +1,5 @@ +import pytest + from pycardano.address import Address from pycardano.crypto.bip32 import HDWallet from pycardano.key import PaymentVerificationKey @@ -108,3 +110,9 @@ def test_payment_address_24_base(): Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() == "addr1qyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sdn8p3d" ) + + +def test_is_entropy(): + entropy = "df9ed25ed146bf43336a5d7cf7395994" + is_entropy = HDWallet.is_entropy(entropy) + assert is_entropy From 679d5331e8463835ae17e50f329df3f51771905e Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 01:02:03 -0700 Subject: [PATCH 05/17] ADD. adding a testcase for creating a HDWallet & a reward address directly from entropy value --- test/pycardano/backend/test_bip32.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index d012cb77..8b65922b 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -1,5 +1,3 @@ -import pytest - from pycardano.address import Address from pycardano.crypto.bip32 import HDWallet from pycardano.key import PaymentVerificationKey @@ -11,6 +9,8 @@ MNEMONIC_15 = "art forum devote street sure rather head chuckle guard poverty release quote oak craft enemy" MNEMONIC_24 = "excess behave track soul table wear ocean cash stay nature item turtle palm soccer lunch horror start stumble month panic right must lock dress" +MNEMONIC_12_ENTROPY = "df9ed25ed146bf43336a5d7cf7395994" + def test_mnemonic(): wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" @@ -112,6 +112,20 @@ def test_payment_address_24_base(): ) +def test_payment_address_12_reward_from_entropy(): + hdwallet = HDWallet.from_entropy(MNEMONIC_12_ENTROPY) + hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET + ).encode() + == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" + ) + + def test_is_entropy(): entropy = "df9ed25ed146bf43336a5d7cf7395994" is_entropy = HDWallet.is_entropy(entropy) From ff447364b1ca16c766e6a6bbb6aa2a077929c889 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 11:44:19 -0700 Subject: [PATCH 06/17] UPDATE. only ignoring bech32.py module from coverage report --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 441e836b..88833c94 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True omit = - pycardano/crypto/* + pycardano/crypto/bech32.py [report] # Regexes for lines to exclude from consideration From 2c2bbebd2a74a8362fb674acd610895195329915 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 11:45:34 -0700 Subject: [PATCH 07/17] ADD. porting over MAINNET address tests from Emurgo's cardano-serialization-lib test cases to increase MAINNET specific test coverage --- test/pycardano/backend/test_bip32.py | 47 ++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index 8b65922b..4dcb7a5d 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -10,6 +10,8 @@ MNEMONIC_24 = "excess behave track soul table wear ocean cash stay nature item turtle palm soccer lunch horror start stumble month panic right must lock dress" MNEMONIC_12_ENTROPY = "df9ed25ed146bf43336a5d7cf7395994" +MNEMONIC_15_ENTROPY = "0ccb74f36b7da1649a8144675522d4d8097c6412" +MNEMONIC_24_ENTROPY = "4e828f9a67ddcff0e6391ad4f26ddb7579f59ba14b6dd4baf63dcfdb9d2420da" def test_mnemonic(): @@ -35,6 +37,13 @@ def test_payment_address_12_reward(): == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" ) + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.MAINNET + ).encode() + == "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz" + ) + def test_payment_address_12_reward2(): hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) @@ -63,6 +72,13 @@ def test_payment_address_12_reward2(): == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" ) + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.MAINNET + ).encode() + == "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz" + ) + def test_payment_address_12_base(): hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) @@ -79,6 +95,11 @@ def test_payment_address_12_base(): == "addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp" ) + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() + == "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwqfjkjv7" + ) + def test_payment_address_15_base(): hdwallet = HDWallet.from_mnemonic(MNEMONIC_15) @@ -90,6 +111,11 @@ def test_payment_address_15_base(): stake_public_key = hdwallet_stake.public_key stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.TESTNET).encode() + == "addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w" + ) + assert ( Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() == "addr1q9u5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qld6xc3" @@ -106,6 +132,11 @@ def test_payment_address_24_base(): stake_public_key = hdwallet_stake.public_key stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + assert ( + Address(spend_vk.hash(), stake_vk.hash(), network=Network.TESTNET).encode() + == "addr_test1qqy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sw96paj" + ) + assert ( Address(spend_vk.hash(), stake_vk.hash(), network=Network.MAINNET).encode() == "addr1qyy6nhfyks7wdu3dudslys37v252w2nwhv0fw2nfawemmn8k8ttq8f3gag0h89aepvx3xf69g0l9pf80tqv7cve0l33sdn8p3d" @@ -125,8 +156,20 @@ def test_payment_address_12_reward_from_entropy(): == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" ) + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.MAINNET + ).encode() + == "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz" + ) + def test_is_entropy(): - entropy = "df9ed25ed146bf43336a5d7cf7395994" - is_entropy = HDWallet.is_entropy(entropy) + is_entropy = HDWallet.is_entropy(MNEMONIC_12_ENTROPY) assert is_entropy + + +def test_is_entropy_wrong_input(): + wrong_entropy = "df9ed25ed146bf43336a5d7cf73959" + is_entropy = HDWallet.is_entropy(wrong_entropy) + assert not is_entropy From eec2609e7d7b93921a95ab154cff2d0fba4acbdc Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 11:48:21 -0700 Subject: [PATCH 08/17] ADD. adding a new phony handle to ease running a single test case --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 39da3ba7..afc24c98 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean clean-test clean-pyc clean-build format test help docs +.PHONY: clean clean-test clean-pyc clean-build format test test-single help docs .DEFAULT_GOAL := help define BROWSER_PYSCRIPT @@ -57,6 +57,9 @@ clean-test: ## remove test and coverage artifacts test: ## runs tests poetry run pytest -s -vv -n 4 +test-single: ## runs tests with "single" markers + poetry run pytest -s -vv -m single + qa: ## runs static analysis with flake8 poetry run flake8 pycardano From 1929601ec0c800bfaa3aea0cdb34920affaef07b Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 11:50:05 -0700 Subject: [PATCH 09/17] ADD. explicitly specifying all phony handles --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index afc24c98..3c42464d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean clean-test clean-pyc clean-build format test test-single help docs +.PHONY: cov cov-html clean clean-test clean-pyc clean-build qa format test test-single help docs .DEFAULT_GOAL := help define BROWSER_PYSCRIPT From 870bc068e67c1c918854423919af86735f200961 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 13:45:07 -0700 Subject: [PATCH 10/17] UPDATE. replacing print statements with logging statements --- pycardano/crypto/bip32.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index d64d1d49..4b6ad143 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -16,6 +16,8 @@ from mnemonic import Mnemonic from nacl import bindings +from pycardano.logging import logger + __all__ = ["BIP32ED25519PrivateKey", "BIP32ED25519PublicKey", "HDWallet"] @@ -581,7 +583,7 @@ def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: else: return Mnemonic(language=language).check(mnemonic=mnemonic) except ValueError: - print( + logger.warning( "The input mnemonic words are not valid. Words should be in string format seperated by space." ) @@ -600,4 +602,5 @@ def is_entropy(entropy: str) -> bool: try: return len(unhexlify(entropy)) in [16, 20, 24, 28, 32] except ValueError: - print("The input entropy is not valid.") + logger.warning("The input entropy is not valid.") + return False From 487962714f5e4d1600243040764ec93727eecaa2 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 13:45:19 -0700 Subject: [PATCH 11/17] ADD. adding more test cases to increase test coverage --- test/pycardano/backend/test_bip32.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index 4dcb7a5d..eaab3258 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -1,3 +1,5 @@ +import pytest + from pycardano.address import Address from pycardano.crypto.bip32 import HDWallet from pycardano.key import PaymentVerificationKey @@ -24,6 +26,12 @@ def test_mnemonic_generation(): assert HDWallet.is_mnemonic(mnemonic_words) +def test_from_mnemonic_invalid_mnemonic(): + wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" + with pytest.raises(ValueError): + HDWallet.from_mnemonic(wrong_mnemonic) + + def test_payment_address_12_reward(): hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") @@ -164,6 +172,11 @@ def test_payment_address_12_reward_from_entropy(): ) +def test_from_entropy_invalid_input(): + with pytest.raises(ValueError): + HDWallet.from_entropy("*(#_") + + def test_is_entropy(): is_entropy = HDWallet.is_entropy(MNEMONIC_12_ENTROPY) assert is_entropy @@ -173,3 +186,8 @@ def test_is_entropy_wrong_input(): wrong_entropy = "df9ed25ed146bf43336a5d7cf73959" is_entropy = HDWallet.is_entropy(wrong_entropy) assert not is_entropy + + +def test_is_entropy_value_error(): + is_entropy = HDWallet.is_entropy("*(#_") + assert is_entropy is False From 4900fceab32d04b11ceb7602564d2c390d2ca0be Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 16:11:47 -0700 Subject: [PATCH 12/17] REFACTOR. Enhancing HDWallet derivation UX by supporting chained execution similar to other popular libraries --- pycardano/crypto/bip32.py | 49 +++++++++++++++------------- test/pycardano/backend/test_bip32.py | 22 ++++--------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 4b6ad143..00ebdcff 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -267,28 +267,26 @@ def derive_from_path(self, path: str, private: bool = True) -> HDWallet: ) derived_hdwallet = self._copy_hdwallet() - for index in path.lstrip("m/").split("/"): if index.endswith("'"): - derived_hdwallet = self.derive_from_index( - derived_hdwallet, int(index[:-1]), private=private, hardened=True + derived_hdwallet = derived_hdwallet.derive( + int(index[:-1]), private=private, hardened=True ) else: - derived_hdwallet = self.derive_from_index( - derived_hdwallet, int(index), private=private, hardened=False + derived_hdwallet = derived_hdwallet.derive( + int(index), private=private, hardened=False ) return derived_hdwallet - def derive_from_index( + def derive( self, - parent_wallet: HDWallet, index: int, private: bool = True, hardened: bool = False, ) -> HDWallet: """ - Derive keys from index. + Returns a new HDWallet derived from given index. Args: index: Derivation index. @@ -301,12 +299,12 @@ def derive_from_index( Examples: >>> mnemonic_words = "test walk nut penalty hip pave soap entry language right filter choice" >>> hdwallet = HDWallet.from_mnemonic(mnemonic_words) - >>> hdwallet_l1 = hdwallet.derive_from_index(parent_wallet=hdwallet, index=1852, hardened=True) - >>> hdwallet_l2 = hdwallet.derive_from_index(parent_wallet=hdwallet_l1, index=1815, hardened=True) - >>> hdwallet_l3 = hdwallet.derive_from_index(parent_wallet=hdwallet_l2, index=0, hardened=True) - >>> hdwallet_l4 = hdwallet.derive_from_index(parent_wallet=hdwallet_l3, index=0) - >>> hdwallet_l5 = hdwallet.derive_from_index(parent_wallet=hdwallet_l4, index=0) - >>> hdwallet_l5.public_key.hex() + >>> hdwallet = hdwallet.derive(index=1852, hardened=True) + >>> hdwallet = hdwallet.derive(index=1815, hardened=True) + >>> hdwallet = hdwallet.derive(index=0, hardened=True) + >>> hdwallet = hdwallet.derive(index=0) + >>> hdwallet = hdwallet.derive(index=0) + >>> hdwallet.public_key.hex() '73fea80d424276ad0978d4fe5310e8bc2d485f5f6bb3bf87612989f112ad5a7d' """ @@ -322,19 +320,19 @@ def derive_from_index( # derive private child key if private: node = ( - parent_wallet._xprivate_key[:32], - parent_wallet._xprivate_key[32:], - parent_wallet._public_key, - parent_wallet._chain_code, - parent_wallet._path, + self._xprivate_key[:32], + self._xprivate_key[32:], + self._public_key, + self._chain_code, + self._path, ) derived_hdwallet = self._derive_private_child_key_by_index(node, index) # derive public child key else: node = ( - parent_wallet._public_key, - parent_wallet._chain_code, - parent_wallet._path, + self._public_key, + self._chain_code, + self._path, ) derived_hdwallet = self._derive_public_child_key_by_index(node, index) @@ -419,7 +417,12 @@ def _derive_private_child_key_by_index( path += "/" + str(index) derived_hdwallet = HDWallet( - xprivate_key=kL + kR, public_key=A, chain_code=c, path=path + xprivate_key=kL + kR, + public_key=A, + chain_code=c, + path=path, + root_xprivate_key=self.root_xprivate_key, + root_public_key=self.root_public_key, ) return derived_hdwallet diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index eaab3258..3cf8292e 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -54,21 +54,13 @@ def test_payment_address_12_reward(): def test_payment_address_12_reward2(): - hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) - hdwalletl_l1 = hdwallet.derive_from_index( - hdwallet, 1852, private=True, hardened=True - ) - hdwalletl_l2 = hdwallet.derive_from_index( - hdwalletl_l1, 1815, private=True, hardened=True - ) - hdwalletl_l3 = hdwallet.derive_from_index( - hdwalletl_l2, 0, private=True, hardened=True - ) - hdwalletl_l4 = hdwallet.derive_from_index( - hdwalletl_l3, 2, private=False, hardened=False - ) - hdwallet_stake = hdwallet.derive_from_index( - hdwalletl_l4, 0, private=False, hardened=False + hdwallet_stake = ( + HDWallet.from_mnemonic(MNEMONIC_12) + .derive(1852, hardened=True) + .derive(1815, hardened=True) + .derive(0, hardened=True) + .derive(2) + .derive(0) ) stake_public_key = hdwallet_stake.public_key stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) From 11e0f0d5c2640e08800f4fde4345c9b5e04d1ebf Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 16:25:23 -0700 Subject: [PATCH 13/17] FIX. ensuring private/public root keys are passed to the child wallet --- pycardano/crypto/bip32.py | 8 +++++++- test/pycardano/backend/test_bip32.py | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 00ebdcff..df8d76c2 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -475,7 +475,13 @@ def _derive_public_child_key_by_index( # compute path path += "/" + str(index) - derived_hdwallet = HDWallet(public_key=A, chain_code=c, path=path) + derived_hdwallet = HDWallet( + public_key=A, + chain_code=c, + path=path, + root_xprivate_key=self.root_xprivate_key, + root_public_key=self.root_public_key, + ) return derived_hdwallet diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index 3cf8292e..74e08f2f 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -59,8 +59,8 @@ def test_payment_address_12_reward2(): .derive(1852, hardened=True) .derive(1815, hardened=True) .derive(0, hardened=True) - .derive(2) - .derive(0) + .derive(2, private=False) + .derive(0, private=False) ) stake_public_key = hdwallet_stake.public_key stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) From 612029b8f860b04480e36d6362bb816394926512 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Mon, 17 Oct 2022 16:41:13 -0700 Subject: [PATCH 14/17] ADD. passing root_chain_code down to derived HDWallet instances --- pycardano/crypto/bip32.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index df8d76c2..306b6c36 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -423,6 +423,7 @@ def _derive_private_child_key_by_index( path=path, root_xprivate_key=self.root_xprivate_key, root_public_key=self.root_public_key, + root_chain_code=self.root_chain_code, ) return derived_hdwallet @@ -481,6 +482,7 @@ def _derive_public_child_key_by_index( path=path, root_xprivate_key=self.root_xprivate_key, root_public_key=self.root_public_key, + root_chain_code=self.root_chain_code, ) return derived_hdwallet From 1aff88a90ef0a9942a8845803716906557b140b3 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Tue, 18 Oct 2022 22:05:56 -0700 Subject: [PATCH 15/17] REFACTOR. pulling out frequently used hard-coded supported mnemonic language list into a constant set --- pycardano/crypto/bip32.py | 45 +++++++++++++-------------------------- 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 306b6c36..3348f127 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -21,6 +21,18 @@ __all__ = ["BIP32ED25519PrivateKey", "BIP32ED25519PublicKey", "HDWallet"] +SUPPORTED_MNEMONIC_LANGS = { + "english", + "french", + "italian", + "japanese", + "chinese_simplified", + "chinese_traditional", + "korean", + "spanish", +} + + class BIP32ED25519PrivateKey: def __init__(self, private_key: bytes, chain_code: bytes): self.private_key = private_key @@ -524,16 +536,7 @@ def generate_mnemonic(language: str = "english", strength: int = 256) -> str: mnemonic (str): mnemonic words. """ - if language and language not in [ - "english", - "french", - "italian", - "japanese", - "chinese_simplified", - "chinese_traditional", - "korean", - "spanish", - ]: + if language and language not in SUPPORTED_MNEMONIC_LANGS: raise ValueError( "invalid language, use only this options english, french, " "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." @@ -559,16 +562,7 @@ def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: bool. Whether the input mnemonic words is valid. """ - if language and language not in [ - "english", - "french", - "italian", - "japanese", - "chinese_simplified", - "chinese_traditional", - "korean", - "spanish", - ]: + if language and language not in SUPPORTED_MNEMONIC_LANGS: raise ValueError( "invalid language, use only this options english, french, " "italian, spanish, chinese_simplified, chinese_traditional, japanese or korean languages." @@ -576,16 +570,7 @@ def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: try: mnemonic = unicodedata.normalize("NFKD", mnemonic) if language is None: - for _language in [ - "english", - "french", - "italian", - "chinese_simplified", - "chinese_traditional", - "japanese", - "korean", - "spanish", - ]: + for _language in SUPPORTED_MNEMONIC_LANGS: valid = False if Mnemonic(language=_language).check(mnemonic=mnemonic) is True: valid = True From cedda9759209c153dc3831969294ef1690ccf8d6 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Tue, 18 Oct 2022 22:24:11 -0700 Subject: [PATCH 16/17] UPDATE. simplifying is_mnemonic() nested loops by supporting early breakout and return statements --- pycardano/crypto/bip32.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pycardano/crypto/bip32.py b/pycardano/crypto/bip32.py index 3348f127..f7efadf5 100644 --- a/pycardano/crypto/bip32.py +++ b/pycardano/crypto/bip32.py @@ -569,15 +569,13 @@ def is_mnemonic(mnemonic: str, language: Optional[str] = None) -> bool: ) try: mnemonic = unicodedata.normalize("NFKD", mnemonic) - if language is None: - for _language in SUPPORTED_MNEMONIC_LANGS: - valid = False - if Mnemonic(language=_language).check(mnemonic=mnemonic) is True: - valid = True - break - return valid - else: + if language: return Mnemonic(language=language).check(mnemonic=mnemonic) + + for _language in SUPPORTED_MNEMONIC_LANGS: + if Mnemonic(language=_language).check(mnemonic=mnemonic) is True: + return True + return False except ValueError: logger.warning( "The input mnemonic words are not valid. Words should be in string format seperated by space." From 899d3d98061dcee8964f0c35d57af978dca6e0c1 Mon Sep 17 00:00:00 2001 From: Daehan Kim Date: Tue, 18 Oct 2022 22:26:36 -0700 Subject: [PATCH 17/17] ADD. adding more testcases for bip32 module coverage --- test/pycardano/backend/test_bip32.py | 68 +++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/test/pycardano/backend/test_bip32.py b/test/pycardano/backend/test_bip32.py index 74e08f2f..96fe9a83 100644 --- a/test/pycardano/backend/test_bip32.py +++ b/test/pycardano/backend/test_bip32.py @@ -16,22 +16,54 @@ MNEMONIC_24_ENTROPY = "4e828f9a67ddcff0e6391ad4f26ddb7579f59ba14b6dd4baf63dcfdb9d2420da" -def test_mnemonic(): +def test_is_mnemonic(): + assert HDWallet.is_mnemonic(MNEMONIC_12) + assert HDWallet.is_mnemonic(MNEMONIC_15) + assert HDWallet.is_mnemonic(MNEMONIC_24) + + +def test_is_mnemonic_language_explicitly_specified(): + assert HDWallet.is_mnemonic(MNEMONIC_12, "english") + + +def test_is_mnemonic_incorrect_mnemonic(): wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" assert not HDWallet.is_mnemonic(wrong_mnemonic) +def test_is_mnemonic_unsupported_language(): + with pytest.raises(ValueError): + HDWallet.is_mnemonic(MNEMONIC_12, language="unsupported language") + + def test_mnemonic_generation(): mnemonic_words = HDWallet.generate_mnemonic(strength=128) assert HDWallet.is_mnemonic(mnemonic_words) +def test_generate_mnemonic_unsupported_lang(): + with pytest.raises(ValueError): + HDWallet.generate_mnemonic(language="unsupported language") + + +def test_generate_mnemonic_unsupported_strength(): + with pytest.raises(ValueError): + HDWallet.generate_mnemonic(strength=64) + + def test_from_mnemonic_invalid_mnemonic(): wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter" with pytest.raises(ValueError): HDWallet.from_mnemonic(wrong_mnemonic) +def test_derive_from_path_incorrect_path(): + root_missing_path = "1852'/1815'/0'/2/0" + with pytest.raises(ValueError): + hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) + hdwallet.derive_from_path(root_missing_path) + + def test_payment_address_12_reward(): hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) hdwallet_stake = hdwallet.derive_from_path("m/1852'/1815'/0'/2/0") @@ -53,6 +85,13 @@ def test_payment_address_12_reward(): ) +def test_payment_address_12_reward2_incorrect_index_value(): + wrong_index_type = "1815" + with pytest.raises(ValueError): + HDWallet.from_mnemonic(MNEMONIC_12)\ + .derive(wrong_index_type, hardened=True) + + def test_payment_address_12_reward2(): hdwallet_stake = ( HDWallet.from_mnemonic(MNEMONIC_12) @@ -80,6 +119,33 @@ def test_payment_address_12_reward2(): ) +def test_payment_address_12_reward2_full_private_derivation(): + hdwallet_stake = ( + HDWallet.from_mnemonic(MNEMONIC_12) + .derive(1852, hardened=True) + .derive(1815, hardened=True) + .derive(0, hardened=True) + .derive(2) + .derive(0) + ) + stake_public_key = hdwallet_stake.public_key + stake_vk = PaymentVerificationKey.from_primitive(stake_public_key) + + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.TESTNET + ).encode() + == "stake_test1uqevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqp8n5xl" + ) + + assert ( + Address( + payment_part=None, staking_part=stake_vk.hash(), network=Network.MAINNET + ).encode() + == "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz" + ) + + def test_payment_address_12_base(): hdwallet = HDWallet.from_mnemonic(MNEMONIC_12) hdwallet_spend = hdwallet.derive_from_path("m/1852'/1815'/0'/0/0")