Skip to content

Commit

Permalink
Add from_descriptor_key classmethod to create HDKey from key expression
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeff S committed Mar 5, 2022
1 parent ac53811 commit 986c650
Show file tree
Hide file tree
Showing 2 changed files with 260 additions and 20 deletions.
166 changes: 146 additions & 20 deletions src/urtypes/crypto/hd_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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]
114 changes: 114 additions & 0 deletions tests/crypto/test_hd_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -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",
},
]

Expand All @@ -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(),
)

0 comments on commit 986c650

Please sign in to comment.