From e0af2afb4c218a4b33920b5bc01121515fa6f9c7 Mon Sep 17 00:00:00 2001 From: dozeee Date: Fri, 22 May 2020 16:30:39 +0200 Subject: [PATCH] integrate yz (uv) extended public keys decode fix check decode on Extended(Private/Public)Key classes add test vectors add vectors for y/z hd keys, add version as extended key instance attribute add check in Hd keys for decoding wrong key type --- btcpy/constants.py | 41 ++++++++++++++++++--- btcpy/structs/hd.py | 90 +++++++++++++++------------------------------ tests/data/hd.json | 23 ++++++++++++ 3 files changed, 88 insertions(+), 66 deletions(-) diff --git a/btcpy/constants.py b/btcpy/constants.py index dc72483..c3c9110 100644 --- a/btcpy/constants.py +++ b/btcpy/constants.py @@ -2,7 +2,6 @@ class Constants(object): - _lookup = {'base58.prefixes': {'1': ('p2pkh', 'mainnet'), 'm': ('p2pkh', 'testnet'), 'n': ('p2pkh', 'testnet'), @@ -16,11 +15,41 @@ class Constants(object): 'testnet': 'tb'}, 'bech32.hrp_to_net': {'bc': 'mainnet', 'tb': 'testnet'}, - 'xkeys.prefixes': {'mainnet': 'x', 'testnet': 't'}, - 'xpub.version': {'mainnet': b'\x04\x88\xb2\x1e', 'testnet': b'\x04\x35\x87\xcf'}, - 'xprv.version': {'mainnet': b'\x04\x88\xad\xe4', 'testnet': b'\x04\x35\x83\x94'}, - 'xpub.prefix': {'mainnet': 'xpub', 'testnet': 'tpub'}, - 'xprv.prefix': {'mainnet': 'xprv', 'testnet': 'tprv'}, + 'xkeys.prefixes': { + b'\x04\x88\xb2\x1e': {'network': 'mainnet', 'prefix': 'x', 'type': 'pub'}, + b'\x04\x88\xad\xe4': {'network': 'mainnet', 'prefix': 'x', 'type': 'prv'}, + + b'\x04\x35\x87\xcf': {'network': 'testnet', 'prefix': 't', 'type': 'pub'}, + b'\x04\x35\x83\x94': {'network': 'testnet', 'prefix': 't', 'type': 'prv'}, + + b'\x04\x9d\x7c\xb2': {'network': 'mainnet', 'prefix': 'y', 'type': 'pub'}, + b'\x04\x9d\x78\x78': {'network': 'mainnet', 'prefix': 'y', 'type': 'prv'}, + + b'\x04\x4a\x52\x62': {'network': 'testnet', 'prefix': 'u', 'type': 'pub'}, + b'\x04\x4a\x4e\x28': {'network': 'testnet', 'prefix': 'u', 'type': 'prv'}, + + b'\x04\xb2\x47\x46': {'network': 'mainnet', 'prefix': 'z', 'type': 'pub'}, + b'\x04\xb2\x43\x0c': {'network': 'mainnet', 'prefix': 'z', 'type': 'prv'}, + + b'\x04\x5f\x1c\xf6': {'network': 'testnet', 'prefix': 'v', 'type': 'pub'}, + b'\x04\x5f\x18\xbc': {'network': 'testnet', 'prefix': 'v', 'type': 'prv'} + }, + 'xkeys.versions': { + 'xpub': b'\x04\x88\xb2\x1e', + 'xprv': b'\x04\x88\xad\xe4', + 'ypub': b'\x04\x9d\x7c\xb2', + 'yprv': b'\x04\x9d\x78\x78', + 'zpub': b'\x04\xb2\x47\x46', + 'zprv': b'\x04\xb2\x43\x0c', + + 'tpub': b'\x04\x35\x87\xcf', + 'tprv': b'\x04\x35\x83\x94', + 'upub': b'\x04\x4a\x52\x62', + 'uprv': b'\x04\x4a\x4e\x28', + 'vpub': b'\x04\x5f\x1c\xf6', + 'vprv': b'\x04\x5f\x18\xbc', + + }, 'wif.prefixes': {'mainnet': 0x80, 'testnet': 0xef}, 'from_unit': Decimal('1e-8'), 'to_unit': Decimal('1e8') diff --git a/btcpy/structs/hd.py b/btcpy/structs/hd.py index 5364ae0..507ecf9 100644 --- a/btcpy/structs/hd.py +++ b/btcpy/structs/hd.py @@ -11,6 +11,7 @@ import hmac from hashlib import sha512 +from itertools import chain from ..lib.base58 import b58decode_check, b58encode_check from ecdsa import VerifyingKey from ecdsa.ellipticcurve import INFINITY @@ -27,34 +28,30 @@ class ExtendedKey(HexSerializable, metaclass=ABCMeta): - - master_parent_fingerprint = bytearray([0]*4) + master_parent_fingerprint = bytearray([0] * 4) first_hardened_index = 1 << 31 curve_order = SECP256k1.order @classmethod - def master(cls, key, chaincode): - return cls(key, chaincode, 0, cls.master_parent_fingerprint, 0, hardened=True) + def master(cls, key, chaincode, version): + return cls(key, chaincode, 0, cls.master_parent_fingerprint, 0, version, hardened=True) @classmethod @strictness def decode(cls, string, strict=None): - - if string[0] == Constants.get('xkeys.prefixes')['mainnet']: - mainnet = True - elif string[0] == Constants.get('xkeys.prefixes')['testnet']: - mainnet = False - else: + decoded = b58decode_check(string) + version = decoded[0:4] + try: + data = Constants.get('xkeys.prefixes')[version] + except KeyError: raise ValueError('Encoded key not recognised: {}'.format(string)) + mainnet = data['network'] == 'mainnet' if strict and mainnet != is_mainnet(): raise ValueError('Trying to decode {}mainnet key ' 'in {}mainnet environment'.format('' if mainnet else 'non-', 'non-' if mainnet else '')) - cls._check_decode(string) - - decoded = b58decode_check(string) parser = Parser(bytearray(decoded)) parser >> 4 depth = int.from_bytes(parser >> 1, 'big') @@ -70,32 +67,23 @@ def decode(cls, string, strict=None): chaincode = parser >> 32 keydata = parser >> 33 - if string[1:4] == 'prv': + if data['type'] == 'prv': subclass = ExtendedPrivateKey - elif string[1:4] == 'pub': + elif data['type'] == 'pub': subclass = ExtendedPublicKey else: raise ValueError('Encoded key not recognised: {}'.format(string)) - + if cls != ExtendedKey and cls != subclass: + raise ValueError('Trying to decode {} key on {} subclass'.format(data['type'], subclass)) key = subclass.decode_key(keydata) - - return subclass(key, chaincode, depth, fingerprint, index, hardened) + return subclass(key, chaincode, depth, fingerprint, index, version, hardened) @staticmethod @abstractmethod def decode_key(keydata): raise NotImplemented - @staticmethod - def _check_decode(string): - pass - - @staticmethod - @abstractmethod - def get_version(mainnet=None): - raise NotImplemented - - def __init__(self, key, chaincode, depth, pfing, index, hardened=False): + def __init__(self, key, chaincode, depth, pfing, index, version, hardened=False): if not 0 <= depth <= 255: raise ValueError('Depth must be between 0 and 255') self.key = key @@ -104,6 +92,7 @@ def __init__(self, key, chaincode, depth, pfing, index, hardened=False): self.parent_fingerprint = pfing self.index = index self.hardened = hardened + self.version = version def derive(self, path): """ @@ -166,7 +155,7 @@ def encode(self, mainnet=None): def serialize(self, mainnet=None): cls = self.__class__ result = Stream() - result << cls.get_version(mainnet) + result << self.version result << self.depth.to_bytes(1, 'big') result << self.parent_fingerprint if self.hardened: @@ -179,7 +168,7 @@ def serialize(self, mainnet=None): def __str__(self): return 'version: {}\ndepth: {}\nparent fp: {}\n' \ - 'index: {}\nchaincode: {}\nkey: {}\nhardened: {}'.format(self.__class__.get_version(), + 'index: {}\nchaincode: {}\nkey: {}\nhardened: {}'.format(self.version, self.depth, self.parent_fingerprint, self.index, @@ -193,31 +182,19 @@ def __eq__(self, other): self.depth == other.depth, self.parent_fingerprint == other.parent_fingerprint, self.index == other.index, + self.version == other.version, self.hardened == other.hardened]) class ExtendedPrivateKey(ExtendedKey): - - @staticmethod - def get_version(mainnet=None): - if mainnet is None: - mainnet = is_mainnet() - # using net_name here would ignore the mainnet=None flag - return Constants.get('xprv.version')['mainnet' if mainnet else 'testnet'] - @staticmethod def decode_key(keydata): return PrivateKey(keydata[1:]) - @staticmethod - def _check_decode(string): - if string[:4] not in (Constants.get('xprv.prefix').values()): - raise ValueError('Non matching prefix: {}'.format(string[:4])) - - def __init__(self, key, chaincode, depth, pfing, index, hardened=False): + def __init__(self, key, chaincode, depth, pfing, index, version, hardened=False): if not isinstance(key, PrivateKey): raise TypeError('ExtendedPrivateKey expects a PrivateKey') - super().__init__(key, chaincode, depth, pfing, index, hardened) + super().__init__(key, chaincode, depth, pfing, index, version, hardened) def __int__(self): return int.from_bytes(self.key.key, 'big') @@ -232,6 +209,7 @@ def get_child(self, index, hardened=False): self.depth + 1, self.get_fingerprint(), index, + self.version, hardened) def get_fingerprint(self): @@ -244,36 +222,27 @@ def _serialized_public(self): return self.pub()._serialize_key() def pub(self): + key_type = Constants.get('xkeys.prefixes')[self.version]['prefix'] + pub_version = Constants.get('xkeys.versions')['{}pub'.format(key_type)] return ExtendedPublicKey(self.key.pub(), self.chaincode, self.depth, self.parent_fingerprint, self.index, + pub_version, self.hardened) class ExtendedPublicKey(ExtendedKey): - @staticmethod - def get_version(mainnet=None): - if mainnet is None: - mainnet = is_mainnet() - # using net_name here would ignore the mainnet=None flag - return Constants.get('xpub.version')['mainnet' if mainnet else 'testnet'] - @staticmethod def decode_key(keydata): return PublicKey(keydata) - @staticmethod - def _check_decode(string): - if string[:4] not in (Constants.get('xpub.prefix').values()): - raise ValueError('Non matching prefix: {}'.format(string[:4])) - - def __init__(self, key, chaincode, depth, pfing, index, hardened=False): + def __init__(self, key, chaincode, depth, pfing, index, version, hardened=False): if not isinstance(key, PublicKey): raise TypeError('ExtendedPublicKey expects a PublicKey') - super().__init__(key.compress(), chaincode, depth, pfing, index, hardened) + super().__init__(key.compress(), chaincode, depth, pfing, index, version, hardened) def __int__(self): return int.from_bytes(self.key.key, 'big') @@ -287,7 +256,8 @@ def get_child(self, index, hardened=False): + VerifyingKey.from_string(self.key.uncompressed[1:], curve=SECP256k1).pubkey.point) if point == INFINITY: raise ValueError('Computed point equals INFINITY') - return ExtendedPublicKey(PublicKey.from_point(point), right, self.depth+1, self.get_fingerprint(), index, False) + return ExtendedPublicKey(PublicKey.from_point(point), right, self.depth + 1, self.get_fingerprint(), index, + self.version, False) def get_hash(self, index, hardened=False): if hardened: diff --git a/tests/data/hd.json b/tests/data/hd.json index 8a7da10..45bb2bc 100644 --- a/tests/data/hd.json +++ b/tests/data/hd.json @@ -68,5 +68,28 @@ "path": "m/0'", "pub": "xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y", "prv": "xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L" + }, + { + "path": "m", + "pub": "zpub6jftahH18ngZxLmXaKw3GSZzZsszmt9WqedkyZdezFtWRFBZqsQH5hyUmb4pCEeZGmVfQuP5bedXTB8is6fTv19U1GQRyQUKQGUTzyHACMF", + "prv": "zprvAWgYBBk7JR8Gjrh4UJQ2uJdG1r3WNRRfURiABBE3RvMXYSrRJL62XuezvGdPvG6GFBZduosCc1YP5wixPox7zhZLfiUm8aunE96BBa4Kei5" + + }, + { + "path": "m/84'/0'/0'", + "pub": "zpub6rFR7y4Q2AijBEqTUquhVz398htDFrtymD9xYYfG1m4wAcvPhXNfE3EfH1r1ADqtfSdVCToUG868RvUUkgDKf31mGDtKsAYz2oz2AGutZYs", + "prv": "zprvAdG4iTXWBoARxkkzNpNh8r6Qag3irQB8PzEMkAFeTRXxHpbF9z4QgEvBRmfvqWvGp42t42nvgGpNgYSJA9iefm1yYNZKEm7z6qUWCroSQnE" + + }, + { + "path": "m", + "pub": "ypub6QqdH2c5z7967ogoqgbD4JeFPmHKhuebp1H8fhzmrK31ZjFqmg4qg93xTWtSzETX5isbBSRSPgBNGrwYE2aHDFMqVim4qVBHmJoefLZduf9", + "prv": "yprvABrGsX5C9januKcLjf4ChAhWqjSqJSvkSnMXsKbAHyW2gvvhE8kb8LjUcE8dUP7fjvrvPB6gPP5baYdsautCeoJwHxncfE26KLYwNc65u29" + + }, + { + "path": "m/49'/0'/0'/0", + "pub": "ypub6ZtnK8aTMxuKkCbteWCBn9vo4D7YXDhCNUGAEqPSY3r1v1D423MxsoBJHBMzVBY4QNEWHe4RtDCKEkhCNn1MxGmdNd76T1SGdf9KZ46gtso", + "prv": "yprvALuRud3ZXbM2XiXRYUfBR1z4WBH47kyM1FLZSSypyiK33CsuUW3iKzrpRufVXR1uKiw6TLt2QEGHupDfwGscoAr23dSjYVVuEozTuCqzjUh" } ] \ No newline at end of file