diff --git a/src/urtypes/crypto/hd_key.py b/src/urtypes/crypto/hd_key.py index fbd4186..bb1b361 100644 --- a/src/urtypes/crypto/hd_key.py +++ b/src/urtypes/crypto/hd_key.py @@ -25,7 +25,7 @@ from urtypes import RegistryType, RegistryItem from urtypes.cbor import DataItem from .coin_info import CoinInfo -from .keypath import Keypath +from .keypath import Keypath, PathComponent CRYPTO_HDKEY = RegistryType("crypto-hdkey", 303) @@ -93,7 +93,7 @@ def bip32_key(self, include_derivation_path=False): ) key = self.key if len(key) == 32: - key = 0x00 + key + key = bytes([0x00]) + key depth = 0 index = 0 if self.master: @@ -117,24 +117,26 @@ def bip32_key(self, include_derivation_path=False): ) if self.parent_fingerprint is not None: parent_fingerprint = self.parent_fingerprint - depth = ( - self.origin.depth - if self.origin.depth is not None - else len(self.origin.components) - ) - paths = self.origin.components - if len(paths) > 0: - last_path = paths[len(paths) - 1] - index = last_path.index - if last_path.hardened: - index += 0x80000000 - if ( - self.parent_fingerprint is None - and self.origin.source_fingerprint is not None - and len(paths) == 1 - ): - parent_fingerprint = self.origin.source_fingerprint - source_is_parent = True + + if self.origin: + depth = ( + self.origin.depth + if self.origin.depth is not None + else len(self.origin.components) + ) + paths = self.origin.components + if len(paths) > 0: + last_path = paths[len(paths) - 1] + index = last_path.index + if last_path.hardened: + index += 0x80000000 + if ( + self.parent_fingerprint is None + and self.origin.source_fingerprint is not None + and len(paths) == 1 + ): + parent_fingerprint = self.origin.source_fingerprint + source_is_parent = True depth = depth.to_bytes(1, "big") index = index.to_bytes(4, "big") key = encode_check( @@ -163,6 +165,87 @@ def bip32_key(self, include_derivation_path=False): def descriptor_key(self): return self.bip32_key(True) + @classmethod + def from_descriptor_key(cls, descriptor_key): + def derivation_to_components(derivation): + levels = derivation.split("/") + components = [] + for level in levels: + hardened = level.endswith("'") or level.lower().endswith("h") + index = level[:-1] if hardened else level + index = None if index == "*" else int(index) + components.append(PathComponent(index, hardened)) + return components + + xkey = descriptor_key + + origin = None + if "[" in descriptor_key: + key_origin, xkey = descriptor_key.split("]", 1) + key_origin = key_origin.lstrip("[") + source_fingerprint, derivation = key_origin.split("/", 1) + components = derivation_to_components(derivation) + if len(components) > 0: + origin = Keypath( + components, + binascii.unhexlify(source_fingerprint), + len(components), + ) + + children = None + if "/" in xkey: + xkey, child_derivation = xkey.split("/", 1) + components = derivation_to_components(child_derivation) + if len(components) > 0: + children = Keypath(components, None, len(components)) + + decoded_key = decode_check(xkey) + + version = decoded_key[:4] + depth = int(decoded_key[4]) + parent_fingerprint = decoded_key[5:9] + chain_code = decoded_key[13:45] + key = decoded_key[45:78] + is_private = key[0] == 0x00 + if is_private: + key = key[1:] + + if origin is not None: + origin.depth = depth + + coin_type = ( + origin.components[1].index + if origin is not None and len(origin.components) > 1 + else 0 + ) + coin_network = ( + 1 + if ( + version == binascii.unhexlify("04358394") + or version == binascii.unhexlify("043587CF") + ) + else 0 + ) + use_info = CoinInfo( + coin_type, + coin_network, + ) + + is_master = parent_fingerprint == binascii.unhexlify("00000000") + + return HDKey( + { + "master": is_master, + "key": key, + "chain_code": chain_code, + "private_key": is_private, + "use_info": use_info, + "origin": origin, + "children": children, + "parent_fingerprint": parent_fingerprint, + } + ) + def to_data_item(self): map = {} if self.master: @@ -254,6 +337,49 @@ def encode(b): return B58_DIGITS[0] * pad + res +def decode(s): + """Decode a base58-encoding string, returning bytes""" + if not s: + return b"" + + # Convert the string to an integer + n = 0 + for c in s: + n *= 58 + if c not in B58_DIGITS: + raise ValueError("Character %r is not a valid base58 character" % c) + digit = B58_DIGITS.index(c) + n += digit + + # Convert the integer to bytes + h = "%x" % n + if len(h) % 2: + h = "0" + h + res = binascii.unhexlify(h.encode("utf8")) + + # Add padding back. + pad = 0 + for c in s[:-1]: + if c == B58_DIGITS[0]: + pad += 1 + else: + break + return b"\x00" * pad + res + + def encode_check(b): """Encode bytes to a base58-encoded string with a checksum""" return encode(b + double_sha256(b)[0:4]) + + +def decode_check(s): + """Decode a base58-encoding string with checksum check. + Returns bytes without checksum + """ + b = decode(s) + checksum = double_sha256(b[:-4])[:4] + if b[-4:] != checksum: + raise ValueError( + "Checksum mismatch: expected %r, calculated %r" % (b[-4:], checksum) + ) + return b[:-4] diff --git a/src/urtypes/crypto/output.py b/src/urtypes/crypto/output.py index 4126e5a..62a5e7c 100644 --- a/src/urtypes/crypto/output.py +++ b/src/urtypes/crypto/output.py @@ -36,6 +36,13 @@ def __init__(self, tag, expression): def __eq__(self, o): return self.tag == o.tag and self.expression == o.expression + @classmethod + def from_script(cls, script): + for script_expression in SCRIPT_EXPRESSION_TAG_MAP.values(): + if script == script_expression.expression: + return script_expression + raise ValueError("unknown script") + SCRIPT_EXPRESSION_TAG_MAP = { 307: ScriptExpression(307, "addr"), diff --git a/src/urtypes/registry.py b/src/urtypes/registry.py index 64fc8c2..7222ad1 100644 --- a/src/urtypes/registry.py +++ b/src/urtypes/registry.py @@ -35,6 +35,10 @@ class RegistryItem: def registry_type(cls): raise NotImplementedError() + @classmethod + def urtype(cls): + return cls.registry_type().type + @classmethod def mapping(cls, item): if isinstance(item, DataItem): diff --git a/tests/crypto/test_hd_key.py b/tests/crypto/test_hd_key.py index 02b8631..510e497 100644 --- a/tests/crypto/test_hd_key.py +++ b/tests/crypto/test_hd_key.py @@ -44,6 +44,7 @@ def table(self): "cbor": binascii.unhexlify( "a301f503582100e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35045820873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" ), + "descriptor_key": "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", }, { "test": "Example/Test Vector 2 (bitcoin testnet public key with derivation path m/44'/1'/1'/0/1)", @@ -73,6 +74,104 @@ def table(self): "cbor": binascii.unhexlify( "a5035821026fe2355745bb2db3630bbc80ef5d58951c963c841f54170ba6e5c12be7fc12a6045820ced155c72456255881793514edc5bd9447e7f74abb88c6d6b6480fd016ee8c8505d90131a1020106d90130a1018a182cf501f501f500f401f4081ae9181cf3" ), + "descriptor_key": "tpubDHW3GtnVrTatx38EcygoSf9UhUd9Dx1rht7FAL8unrMo8r2NWhJuYNqDFS7cZFVbDaxJkV94MLZAr86XFPsAPYcoHWJ7sWYsrmHDw5sKQ2K", + }, + ] + + def descriptor_table(self): + return [ + { + "test": "master key", + "item": HDKey( + { + "master": True, + "key": binascii.unhexlify( + "00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" + ), + "chain_code": binascii.unhexlify( + "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" + ), + } + ), + "descriptor_key": "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + }, + { + "test": "xpub is a child of a master with fingerprint d34db33f, and derived using path 44'/0'/0'", + "item": HDKey( + { + "key": binascii.unhexlify( + "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0" + ), + "chain_code": binascii.unhexlify( + "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29" + ), + "origin": Keypath( + [ + PathComponent(44, True), + PathComponent(0, True), + PathComponent(0, True), + ], + binascii.unhexlify("d34db33f"), + None, + ), + "parent_fingerprint": binascii.unhexlify("78412e3a"), + } + ), + "descriptor_key": "[d34db33f/44'/0'/0']xpub6CY2xt3mvQejPFUw26CychtL4GMq1yp41aMW2U27mvThqefpZYwXpGscV26JuVj13Fpg4kgSENheUSbTqm5f8z25zrhXpPVss5zWeMGnAKR", + }, + { + "test": "xpub is a child of a master with fingerprint d34db33f, and derived using path 44'/0'/0', with child derivation", + "item": HDKey( + { + "key": binascii.unhexlify( + "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0" + ), + "chain_code": binascii.unhexlify( + "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29" + ), + "origin": Keypath( + [ + PathComponent(44, True), + PathComponent(0, True), + PathComponent(0, True), + ], + binascii.unhexlify("d34db33f"), + None, + ), + "children": Keypath( + [PathComponent(1, False), PathComponent(None, False)], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify("78412e3a"), + } + ), + "descriptor_key": "[d34db33f/44'/0'/0']xpub6CY2xt3mvQejPFUw26CychtL4GMq1yp41aMW2U27mvThqefpZYwXpGscV26JuVj13Fpg4kgSENheUSbTqm5f8z25zrhXpPVss5zWeMGnAKR/1/*", + }, + { + "test": "m/84'/1'/0'", + "item": HDKey( + { + "key": binascii.unhexlify( + "0204ab245e5417bcdc52e2a6b92fafa2c8ce54ba97d4b1216f074915870000f946" + ), + "chain_code": binascii.unhexlify( + "9c6bf9263cab713ed098edcd147c651dfc924d953c11ad095a70f9bd31de1d8c" + ), + "use_info": CoinInfo(None, 1), + "origin": Keypath( + [ + PathComponent(84, True), + PathComponent(1, True), + PathComponent(0, True), + ], + binascii.unhexlify("55f8fc5d"), + 3, + ), + "parent_fingerprint": binascii.unhexlify("1b01c99c"), + } + ), + "descriptor_key": "[55f8fc5d/84'/1'/0']tpubDCDuqu5HtBX2aD7wxvnHcj1DgFN1UVgzLkA1Ms4Va4P7TpJ3jDknkPLwWT2SqrKXNNAtJBCPcbJ8Tcpm6nLxgFapCZyhKgqwcEGv1BVpD7s", }, ] @@ -83,3 +182,18 @@ def test_from_cbor(self): def test_to_cbor(self): for row in self.table(): self.assertEqual(row["item"].to_cbor(), row["cbor"]) + + def test_descriptor_key(self): + for row in self.descriptor_table(): + self.assertEqual(row["item"].descriptor_key(), row["descriptor_key"]) + + def test_from_descriptor_key(self): + for row in self.descriptor_table(): + self.assertEqual( + HDKey.from_descriptor_key(row["descriptor_key"]).descriptor_key(), + row["descriptor_key"], + ) + self.assertEqual( + HDKey.from_descriptor_key(row["descriptor_key"]).descriptor_key(), + row["item"].descriptor_key(), + )