diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1dc6f214a4a529180ca25ce4ad5edf27c0e022ac..8e8fca2d4fe4e71d43dd22e2a7186f6092f984c6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,8 @@ Changelog 0.23.4 ------ * Bip39 and Bip32 support has been added +* Privatekey derivation based on Bip39/Bip22 has been added +* Several unit tests have been added 0.23.3 ------ diff --git a/beemgraphenebase/__init__.py b/beemgraphenebase/__init__.py index 54d00b2e6684ad3692c27a30a4f024ff5a09259d..487c905f10355d430333de3a4cb423d87a636f66 100644 --- a/beemgraphenebase/__init__.py +++ b/beemgraphenebase/__init__.py @@ -9,6 +9,7 @@ from .version import version as __version__ __all__ = ['account', 'base58', + 'bip32' 'bip38', 'transactions', 'types', diff --git a/beemgraphenebase/account.py b/beemgraphenebase/account.py index 90d22c7373cf3d399590047574a944d6f6635151..3bcc17db9fd5bec62e792c0a993fec0059b1569d 100644 --- a/beemgraphenebase/account.py +++ b/beemgraphenebase/account.py @@ -22,6 +22,7 @@ from binascii import hexlify, unhexlify import unicodedata from .base58 import ripemd160, Base58 +from .bip32 import BIP32Key, parse_path from .dictionary import words as BrainKeyDictionary from .dictionary import words_bip39 as MnemonicDictionary from .py23 import py23_bytes, PY2 @@ -176,11 +177,17 @@ class BrainKey(object): # Copyright (c) 2017 mruddy @python_2_unicode_compatible class Mnemonic(object): + """BIP39 mnemoric implementation""" def __init__(self): self.wordlist = MnemonicDictionary.split(',') - self.radix = 2048 + self.radix = 2048 def generate(self, strength=128): + """ Generates a word list based on the given strength + + :param int strength: initial entropy strength, must be one of [128, 160, 192, 224, 256] + + """ if strength not in [128, 160, 192, 224, 256]: raise ValueError( "Strength should be one of the following [128, 160, 192, 224, 256], but it is not (%d)." @@ -267,6 +274,10 @@ class Mnemonic(object): return result_phrase def check(self, mnemonic): + """ Checks the mnemonic word list is valid + :param list mnemonic: mnemonic word list with lenght of 12, 15, 18, 21, 24 + :returns: True, when valid + """ mnemonic = self.normalize_string(mnemonic).split(" ") # list of valid mnemonic lengths if len(mnemonic) not in [12, 15, 18, 21, 24]: @@ -284,6 +295,11 @@ class Mnemonic(object): return h == nh def expand_word(self, prefix): + """Expands a word when sufficient chars are given + + :param str prefix: first chars of a valid dict word + + """ if prefix in self.wordlist: return prefix else: @@ -296,10 +312,12 @@ class Mnemonic(object): return prefix def expand(self, mnemonic): + """Expands all words given in a list""" return " ".join(map(self.expand_word, mnemonic.split(" "))) @classmethod def normalize_string(cls, txt): + """Normalizes strings""" if isinstance(txt, str if sys.version < "3" else bytes): utxt = txt.decode("utf8") elif isinstance(txt, unicode if sys.version < "3" else str): # noqa: F821 @@ -311,6 +329,12 @@ class Mnemonic(object): @classmethod def to_seed(cls, mnemonic, passphrase=""): + """Returns a seed based on bip39 + + :param str mnemonic: string containing a valid mnemonic word list + :param str passphrase: optional, passphrase can be set to modify the returned seed. + + """ mnemonic = cls.normalize_string(mnemonic) passphrase = cls.normalize_string(passphrase) passphrase = "mnemonic" + passphrase @@ -320,6 +344,54 @@ class Mnemonic(object): return stretched[:64] + + +class MnemonicKey(object): + """ This class derives a private key from a BIP39 mnemoric implementation + """ + + def __init__(self, word_list, passphrase="", role="active", account_number=0, sequence=0, prefix="STM"): + mnemonic = Mnemonic() + self.seed = mnemonic.to_seed(word_list, passphrase=passphrase) + self.role = role + self.account_number = account_number + self.sequence = sequence + self.prefix = prefix + self.path_prefix = "m/48'/13'" + + def get_path(self): + if self.account_number < 0: + raise ValueError("sequence must be >= 0") + if self.sequence < 0: + raise ValueError("account_number must be >= 0") + if self.role == "owner": + return "%s/0'/%d'/%d'" % (self.path_prefix, self.account_number, self.sequence) + elif self.role == "active": + return "%s/1'/%d'/%d'" % (self.path_prefix, self.account_number, self.sequence) + elif self.role == "posting": + return "%s/4'/%d'/%d'" % (self.path_prefix, self.account_number, self.sequence) + elif self.role == "memo": + return "%s/3'/%d'/%d'" % (self.path_prefix, self.account_number, self.sequence) + raise ValueError("Wrong role") + + def get_private(self): + """ Derive private key from the account_number, the role and the sequence + """ + key = BIP32Key.fromEntropy(self.seed) + for n in parse_path(self.get_path()): + key = key.ChildKey(n) + return PrivateKey(key.WalletImportFormat(), prefix=self.prefix) + + def get_public(self): + return self.get_private().pubkey + + def get_private_key(self): + return self.get_private() + + def get_public_key(self): + return self.get_public() + + @python_2_unicode_compatible class Address(object): """ Address class diff --git a/beemgraphenebase/base58.py b/beemgraphenebase/base58.py index 7cd2256899391cbfdb8427f234a64f871864753f..b4cf72487dffc6ab5d915c5cc2c336855a183a5f 100644 --- a/beemgraphenebase/base58.py +++ b/beemgraphenebase/base58.py @@ -192,13 +192,16 @@ def base58CheckEncode(version, payload): return base58encode(result) -def base58CheckDecode(s): +def base58CheckDecode(s, skip_first_bytes=True): s = unhexlify(base58decode(s)) dec = hexlify(s[:-4]).decode('ascii') checksum = doublesha256(dec)[:4] if not (s[-4:] == checksum): raise AssertionError() - return dec[2:] + if skip_first_bytes: + return dec[2:] + else: + return dec def gphBase58CheckEncode(s): diff --git a/beemgraphenebase/bip32.py b/beemgraphenebase/bip32.py index 99e9c2a637c7ab8336c2c2c416c15f069b300c82..3e1ffe8ba75605cae1ce048a41cb606739d0450c 100644 --- a/beemgraphenebase/bip32.py +++ b/beemgraphenebase/bip32.py @@ -75,7 +75,7 @@ class BIP32Key(object): """ # Sanity checks # raw = check_decode(xkey) - raw = b'\x04' + unhexlify(base58CheckDecode(xkey)) + raw = unhexlify(base58CheckDecode(xkey, skip_first_bytes=False)) if len(raw) != 78: raise ValueError("extended key format wrong length") diff --git a/tests/beem/test_cli.py b/tests/beem/test_cli.py index 4db91caea9f24d59d863043524a47150b3cd1cca..9e736dcbd0176be4d47b23b82f69176ba9b39d78 100644 --- a/tests/beem/test_cli.py +++ b/tests/beem/test_cli.py @@ -455,9 +455,9 @@ class Testcases(unittest.TestCase): self.assertEqual(result.exit_code, 0) result = runner.invoke(cli, ['pending', '--post', '--comment', account_name]) self.assertEqual(result.exit_code, 0) - result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', '--permlink', '--days', '1', account_name]) + result = runner.invoke(cli, ['pending', '--curation', '--permlink', '--days', '1', account_name]) self.assertEqual(result.exit_code, 0) - result = runner.invoke(cli, ['pending', '--post', '--comment', '--curation', '--author', '--permlink', '--length', '30', '--days', '1', account_name]) + result = runner.invoke(cli, ['pending', '--post', '--comment', '--author', '--permlink', '--length', '30', '--days', '1', account_name]) self.assertEqual(result.exit_code, 0) result = runner.invoke(cli, ['pending', '--post', '--author', '--title', '--days', '1', account_name]) self.assertEqual(result.exit_code, 0) diff --git a/tests/beemgraphene/test_account.py b/tests/beemgraphene/test_account.py index 4122940d4b32f409850c259e96eb2371b4092e5a..c1d0bfb0ed643863e358da1fb91ce620b087d38f 100644 --- a/tests/beemgraphene/test_account.py +++ b/tests/beemgraphene/test_account.py @@ -7,7 +7,7 @@ from builtins import str import unittest from beemgraphenebase.base58 import Base58, base58encode from beemgraphenebase.bip32 import BIP32Key -from beemgraphenebase.account import BrainKey, Address, PublicKey, PrivateKey, PasswordKey, Mnemonic +from beemgraphenebase.account import BrainKey, Address, PublicKey, PrivateKey, PasswordKey, Mnemonic, MnemonicKey from binascii import hexlify, unhexlify import sys import hashlib @@ -452,3 +452,29 @@ class Testcases(unittest.TestCase): m = Mnemonic() for d in data: self.assertEqual(m.to_entropy(m.to_mnemonic(d).split()), d) + + def test_mnemorickey(self): + word_list = "news clever spot drama infant detail sword cover color throw foot primary when slender rhythm clog autumn ecology enough bronze math you modify excuse" + mk = MnemonicKey(word_list, role="owner") + self.assertEqual(str(mk.get_private_key()), str(PrivateKey("L2cgypn75kre1s7JUkTK6H7Y656GwDbNnSNZKWSQ2Rnnx6qM11KD"))) + self.assertEqual(str(mk.get_public_key()), str(PublicKey("02821aa2d26c4fd9b735dd1fed148b96fec978eae1440adf79a4bf95e118b2d8f1"))) + + mk = MnemonicKey(word_list, role="owner", sequence=5) + self.assertEqual(str(mk.get_private_key()), str(PrivateKey("L5cJSZPcBMBtmuRaK9C48yyXK5JpaH15BsjKLZkmamEWKKx25Kx7"))) + self.assertEqual(str(mk.get_public_key()), str(PublicKey("0309a45aa9add2c7421e0553e23b1800e51d95e525fa4eae1bcc5fb58186e07ed5"))) + + mk = MnemonicKey(word_list, role="owner", account_number=2) + self.assertEqual(str(mk.get_private_key()), str(PrivateKey("L4A95nfp1kyJtUtaTqzMyLQkz6NYSfk4R8pejcgKXQSUtPysiFgv"))) + self.assertEqual(str(mk.get_public_key()), str(PublicKey("02b164dc8819830cd50fca4217ad35fa7371cf29db1bc6a07456cc0090ca8ea8fe"))) + + mk = MnemonicKey(word_list, role="active") + self.assertEqual(str(mk.get_private_key()), str(PrivateKey("KygWePihetfhKYCHahcHjMebNSy53HtcXkccYuTn6R8QLydyPUWt"))) + self.assertEqual(str(mk.get_public_key()), str(PublicKey("035c679454155c4c41e8956ecb8e514d37d2d28da91db81c3a22f216a09af94605"))) + + mk = MnemonicKey(word_list, role="posting") + self.assertEqual(str(mk.get_private_key()), str(PrivateKey("L53K986B756VbqatsCi3jjPLHCq8Y38AZyXf19w6CcxuGH84Rrhs"))) + self.assertEqual(str(mk.get_public_key()), str(PublicKey("0200e7d987dfd5aaecf822420475ddcbadc8503a99893d50d06f87da48e85a8206"))) + + mk = MnemonicKey(word_list, role="memo") + self.assertEqual(str(mk.get_private_key()), str(PrivateKey("L5GrFqdRsroM1Ym4aMdALQBL7xN9kNMru9JTgtwbHVZ4iGvx1184"))) + self.assertEqual(str(mk.get_public_key()), str(PublicKey("02fa2cdf5a007b01b1911615a4fba9c2a864a1c1ed079d222e5d549d207412c601"))) diff --git a/tests/beemgraphene/test_bip32.py b/tests/beemgraphene/test_bip32.py index 46555285a65af085ab9a71e2fa57431e0e8f612a..db6c8363364ac0c53a8166276f4c50cf4af8f04b 100644 --- a/tests/beemgraphene/test_bip32.py +++ b/tests/beemgraphene/test_bip32.py @@ -113,6 +113,56 @@ class Testcases(unittest.TestCase): self.assertEqual(m.ExtendedKey(), "xprvA41z7zogVVwxVSgdKUHDy1SKmdb533PjDz7J6N6mV6uS3ze1ai8FHa8kmHScGpWmj4WggLyQjgPie1rFSruoUihUZREPSL39UNdE3BBDu76") self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub6H1LXWLaKsWFhvm6RVpEL9P4KfRZSW7abD2ttkWP3SSQvnyA8FSVqNTEcYFgJS2UaFcxupHiYkro49S8yGasTvXEYBVPamhGW6cFJodrTHy") + def test_vector2(self): + seed = unhexlify("fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542") + key = BIP32Key.fromEntropy(seed) + self.assertEqual(key.ExtendedKey(), "xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U") + self.assertEqual(key.ExtendedKey(private=False), "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB") + + path = "m/0" + m = key + for n in parse_path(path): + m = m.ChildKey(n) + self.assertEqual(m.ExtendedKey(), "xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt") + self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH") + + path = "m/0/2147483647'" + m = key + for n in parse_path(path): + m = m.ChildKey(n) + self.assertEqual(m.ExtendedKey(), "xprv9wSp6B7kry3Vj9m1zSnLvN3xH8RdsPP1Mh7fAaR7aRLcQMKTR2vidYEeEg2mUCTAwCd6vnxVrcjfy2kRgVsFawNzmjuHc2YmYRmagcEPdU9") + self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub6ASAVgeehLbnwdqV6UKMHVzgqAG8Gr6riv3Fxxpj8ksbH9ebxaEyBLZ85ySDhKiLDBrQSARLq1uNRts8RuJiHjaDMBU4Zn9h8LZNnBC5y4a") + path = "m/0/2147483647'/1" + m = key + for n in parse_path(path): + m = m.ChildKey(n) + self.assertEqual(m.ExtendedKey(), "xprv9zFnWC6h2cLgpmSA46vutJzBcfJ8yaJGg8cX1e5StJh45BBciYTRXSd25UEPVuesF9yog62tGAQtHjXajPPdbRCHuWS6T8XA2ECKADdw4Ef") + self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub6DF8uhdarytz3FWdA8TvFSvvAh8dP3283MY7p2V4SeE2wyWmG5mg5EwVvmdMVCQcoNJxGoWaU9DCWh89LojfZ537wTfunKau47EL2dhHKon") + path = "m/0/2147483647'/1/2147483646'" + m = key + for n in parse_path(path): + m = m.ChildKey(n) + self.assertEqual(m.ExtendedKey(), "xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc") + self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL") + path = "m/0/2147483647'/1/2147483646'/2" + m = key + for n in parse_path(path): + m = m.ChildKey(n) + self.assertEqual(m.ExtendedKey(), "xprvA2nrNbFZABcdryreWet9Ea4LvTJcGsqrMzxHx98MMrotbir7yrKCEXw7nadnHM8Dq38EGfSh6dqA9QWTyefMLEcBYJUuekgW4BYPJcr9E7j") + self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub6FnCn6nSzZAw5Tw7cgR9bi15UV96gLZhjDstkXXxvCLsUXBGXPdSnLFbdpq8p9HmGsApME5hQTZ3emM2rnY5agb9rXpVGyy3bdW6EEgAtqt") + + def test_vector3(self): + seed = unhexlify("4b381541583be4423346c643850da4b320e46a87ae3d2a4e6da11eba819cd4acba45d239319ac14f863b8d5ab5a0d0c64d2e8a1e7d1457df2e5a3c51c73235be") + key = BIP32Key.fromEntropy(seed) + self.assertEqual(key.ExtendedKey(), "xprv9s21ZrQH143K25QhxbucbDDuQ4naNntJRi4KUfWT7xo4EKsHt2QJDu7KXp1A3u7Bi1j8ph3EGsZ9Xvz9dGuVrtHHs7pXeTzjuxBrCmmhgC6") + self.assertEqual(key.ExtendedKey(private=False), "xpub661MyMwAqRbcEZVB4dScxMAdx6d4nFc9nvyvH3v4gJL378CSRZiYmhRoP7mBy6gSPSCYk6SzXPTf3ND1cZAceL7SfJ1Z3GC8vBgp2epUt13") + + path = "m/0'" + m = key + for n in parse_path(path): + m = m.ChildKey(n) + self.assertEqual(m.ExtendedKey(), "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L") + self.assertEqual(m.ExtendedKey(private=False, encoded=True), "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y") if __name__ == '__main__': unittest.main()