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

integrate yz (uv) extended keys #58

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions btcpy/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class Constants(object):

_lookup = {'base58.prefixes': {'1': ('p2pkh', 'mainnet'),
'm': ('p2pkh', 'testnet'),
'n': ('p2pkh', 'testnet'),
Expand All @@ -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')
Expand Down
90 changes: 30 additions & 60 deletions btcpy/structs/hd.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -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
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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')
Expand All @@ -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):
Expand All @@ -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')
Expand All @@ -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:
Expand Down
23 changes: 23 additions & 0 deletions tests/data/hd.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]