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

Descriptor key parsing and helper methods #4

Open
wants to merge 3 commits into
base: main
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
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]
7 changes: 7 additions & 0 deletions src/urtypes/crypto/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 4 additions & 0 deletions src/urtypes/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
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(),
)