Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixing inconsistency between generated entropy value type and the expected HDWallet.entropy value type #101

Merged
merged 17 commits into from Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
682cc9b
UPDATE. ignoring common IDE directories
daehan-koreapool Oct 17, 2022
5ecc142
UPDATE. ensuring entropy value stays as a string value for consistency
daehan-koreapool Oct 17, 2022
692682e
FIX. correctly converting bytearray into hex decoded string value
daehan-koreapool Oct 17, 2022
d6838b3
UPDATE. explicitly requiring from_entropy() method to have a string d…
daehan-koreapool Oct 17, 2022
679d533
ADD. adding a testcase for creating a HDWallet & a reward address dir…
daehan-koreapool Oct 17, 2022
ff44736
UPDATE. only ignoring bech32.py module from coverage report
daehan-koreapool Oct 17, 2022
2c2bbeb
ADD. porting over MAINNET address tests from Emurgo's cardano-seriali…
daehan-koreapool Oct 17, 2022
eec2609
ADD. adding a new phony handle to ease running a single test case
daehan-koreapool Oct 17, 2022
1929601
ADD. explicitly specifying all phony handles
daehan-koreapool Oct 17, 2022
870bc06
UPDATE. replacing print statements with logging statements
daehan-koreapool Oct 17, 2022
4879627
ADD. adding more test cases to increase test coverage
daehan-koreapool Oct 17, 2022
4900fce
REFACTOR. Enhancing HDWallet derivation UX by supporting chained exec…
daehan-koreapool Oct 17, 2022
11e0f0d
FIX. ensuring private/public root keys are passed to the child wallet
daehan-koreapool Oct 17, 2022
612029b
ADD. passing root_chain_code down to derived HDWallet instances
daehan-koreapool Oct 17, 2022
1aff88a
REFACTOR. pulling out frequently used hard-coded supported mnemonic l…
daehan-koreapool Oct 19, 2022
cedda97
UPDATE. simplifying is_mnemonic() nested loops by supporting early br…
daehan-koreapool Oct 19, 2022
899d3d9
ADD. adding more testcases for bip32 module coverage
daehan-koreapool Oct 19, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coveragerc
@@ -1,7 +1,7 @@
[run]
branch = True
omit =
pycardano/crypto/*
pycardano/crypto/bech32.py

[report]
# Regexes for lines to exclude from consideration
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,8 @@
.coverage
cov_html
docs/build
dist
dist

# IDE
.idea
.code
5 changes: 4 additions & 1 deletion Makefile
@@ -1,4 +1,4 @@
.PHONY: clean clean-test clean-pyc clean-build format test 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
Expand Down Expand Up @@ -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

Expand Down
40 changes: 22 additions & 18 deletions pycardano/crypto/bip32.py
Expand Up @@ -16,6 +16,8 @@
from mnemonic import Mnemonic
from nacl import bindings

from pycardano.logging import logger

__all__ = ["BIP32ED25519PrivateKey", "BIP32ED25519PublicKey", "HDWallet"]


Expand Down Expand Up @@ -109,6 +111,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.
Expand Down Expand Up @@ -152,28 +157,18 @@ 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 ""
daehan-koreapool marked this conversation as resolved.
Show resolved Hide resolved
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(),
mnemonic=mnemonic,
entropy=entropy,
entropy=hexlify(entropy).decode("utf-8"),
passphrase=passphrase,
)

@classmethod
def from_entropy(cls, entropy: str, passphrase: str = None) -> HDWallet:
def from_entropy(cls, entropy: str, passphrase: str = "") -> HDWallet:
daehan-koreapool marked this conversation as resolved.
Show resolved Hide resolved
"""
Create master key and HDWallet from Mnemonic words.

Expand All @@ -188,12 +183,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:
Expand Down Expand Up @@ -580,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."
)

Expand All @@ -599,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
83 changes: 83 additions & 0 deletions 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
Expand All @@ -9,6 +11,10 @@
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"
MNEMONIC_15_ENTROPY = "0ccb74f36b7da1649a8144675522d4d8097c6412"
MNEMONIC_24_ENTROPY = "4e828f9a67ddcff0e6391ad4f26ddb7579f59ba14b6dd4baf63dcfdb9d2420da"


def test_mnemonic():
wrong_mnemonic = "test walk nut penalty hip pave soap entry language right filter"
Expand All @@ -20,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")
Expand All @@ -33,6 +45,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)
Expand Down Expand Up @@ -61,6 +80,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)
Expand All @@ -77,6 +103,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)
Expand All @@ -88,6 +119,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"
Expand All @@ -104,7 +140,54 @@ 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"
)


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"
)

assert (
Address(
payment_part=None, staking_part=stake_vk.hash(), network=Network.MAINNET
).encode()
== "stake1uyevw2xnsc0pvn9t9r9c7qryfqfeerchgrlm3ea2nefr9hqxdekzz"
)


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


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