Skip to content

Commit

Permalink
Multiwallet tweaks (#83)
Browse files Browse the repository at this point in the history
* small cleanups & caravan export
  • Loading branch information
mflaxman authored Aug 4, 2021
1 parent 1d1e900 commit 6c3a0ad
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 132 deletions.
86 changes: 56 additions & 30 deletions buidl/descriptor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re

from buidl.hd import HDPublicKey, is_valid_bip32_path
from buidl.helper import sha256, uses_only_hex_chars
from buidl.helper import is_intable, sha256, uses_only_hex_chars
from buidl.op import number_to_op_code
from buidl.script import P2WSHScriptPubKey, WitnessScript

Expand Down Expand Up @@ -67,41 +67,42 @@ def is_valid_xfp_hex(string):

def parse_full_key_record(key_record_str):
"""
A full key record will come from your Coordinator and include a reference to change derivation.
A full key record will come from your Coordinator and include a reference to an account index.
It will look something like this:
[c7d0648a/48h/1h/0h/2h]tpubDEpefcgzY6ZyEV2uF4xcW2z8bZ3DNeWx9h2BcwcX973BHrmkQxJhpAXoSWZeHkmkiTtnUjfERsTDTVCcifW6po3PFR1JRjUUTJHvPpDqJhr/0/*'
[c7d0648a/48h/1h/0h/2h]tpubDEpefcgzY6ZyEV2uF4xcW2z8bZ3DNeWx9h2BcwcX973BHrmkQxJhpAXoSWZeHkmkiTtnUjfERsTDTVCcifW6po3PFR1JRjUUTJHvPpDqJhr/0/*
A full key record is basically a partial key record, with a trailing /{account-index}/*
"""

key_record_re = re.match(
r"\[([0-9a-f]{8})(.*?)\]([0-9A-Za-z]+)\/([0-9]+?)\/\*", key_record_str
)
if key_record_re is None:
raise ValueError(f"Invalid key record: {key_record_str}")
# Validate that it appears to be a full key record:
parts = key_record_str.split("/")
if parts[-1] != "*":
raise ValueError(
"Invalid full key record, does not end with a *: {key_record_str}"
)
if not is_intable(parts[-2]):
raise ValueError(
"Invalid full key record, account index `{parts[-2]}` is not an int: {key_record_str}"
)

xfp, path, xpub, index_str = key_record_re.groups()
# Note that we don't validate xfp/index because the regex already tells us they're composed of ints
# Now we strip off the trailing account index and *, and parse the rest as a partial key record
partial_key_record_str = "/".join(parts[0 : len(parts) - 2])

index_int = int(index_str)

path = "m" + path
if not is_valid_bip32_path(path):
raise ValueError(f"Invalid BIP32 path {path} in key record: {key_record_str}")
to_return = parse_partial_key_record(key_record_str=partial_key_record_str)
to_return["account_index"] = int(parts[-2])
to_return["xpub_parent"] = to_return.pop("xpub")

try:
parent_pubkey_obj = HDPublicKey.parse(s=xpub)
network = parent_pubkey_obj.network
xpub_child = parent_pubkey_obj.child(index=index_int).xpub()
parent_pubkey_obj = HDPublicKey.parse(s=to_return["xpub_parent"])
to_return["xpub_child"] = parent_pubkey_obj.child(
index=to_return["account_index"]
).xpub()
except ValueError:
raise ValueError(f"Invalid xpub {xpub} in key record: {key_record_str}")
raise ValueError(
f"Invalid parent xpub {to_return['xpub_parent']} in key record: {key_record_str}"
)

return {
"xfp": xfp,
"path": path,
"xpub_parent": xpub,
"account_index": index_int,
"xpub_child": xpub_child,
"network": network,
}
return to_return


def parse_partial_key_record(key_record_str):
Expand Down Expand Up @@ -138,6 +139,17 @@ def parse_partial_key_record(key_record_str):
}


def parse_any_key_record(key_record_str):
"""
Try to parse a key record as full key record, and if not parse as a partial key record
"""
try:
return parse_full_key_record(key_record_str)
except ValueError:
return parse_partial_key_record(key_record_str)


class P2WSHSortedMulti:

# TODO: make an inheritable base descriptor class that this inherits from
Expand All @@ -147,6 +159,7 @@ def __init__(
quorum_m, # m as in m-of-n
key_records=[], # pubkeys required to sign
checksum="",
sort_key_records=True,
):
if type(quorum_m) is not int or quorum_m < 1:
raise ValueError(f"quorum_m must be a positive int: {quorum_m}")
Expand All @@ -156,7 +169,6 @@ def __init__(
raise ValueError("No key_records supplied")

key_records_to_save, network = [], None
descriptor_text = f"wsh(sortedmulti({quorum_m}"
for key_record in key_records:
# TODO: does bitcoin core have a standard to enforce for h vs ' in bip32 path?
path = key_record.get("path")
Expand Down Expand Up @@ -209,14 +221,23 @@ def __init__(
}
)

descriptor_text += f",[{key_record['xfp']}{key_record['path'][1:]}]{xpub_to_use}/{account_index}/*"
if sort_key_records:
# Sort lexicographically based on parent xpub
key_records_to_save = sorted(
key_records_to_save, key=lambda k: k["xpub_parent"]
)

# Generate descriptor text (not part of above loop due to sort_key_records)
descriptor_text = f"wsh(sortedmulti({quorum_m}"
for kr in key_records_to_save:
descriptor_text += f",[{kr['xfp']}{kr['path'][1:]}]{kr['xpub_parent']}/{kr['account_index']}/*"
descriptor_text += "))"
self.descriptor_text = descriptor_text
self.key_records = key_records_to_save
self.network = network

calculated_checksum = calc_core_checksum(descriptor_text)

if checksum:
# test that it matches
if calculated_checksum != checksum:
Expand Down Expand Up @@ -274,7 +295,12 @@ def parse(cls, output_record):
f"Malformed threshold {quorum_m_int}-of-{len(key_records)} (m must be less than n) in {output_record}"
)

return cls(quorum_m=quorum_m_int, key_records=key_records, checksum=checksum)
return cls(
quorum_m=quorum_m_int,
key_records=key_records,
sort_key_records=False,
checksum=checksum,
)

def get_address(self, offset=0, is_change=False, sort_keys=True):
"""
Expand Down
2 changes: 1 addition & 1 deletion buidl/psbt_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def create_p2sh_multisig_psbt(
for xfp_hex, base_paths in xpubs_dict.items():
for base_path in base_paths:

hd_pubkey_obj = HDPublicKey.parse(base_path["xpub_hex"])
hd_pubkey_obj = HDPublicKey.parse(base_path["xpub_b58"])

# We will use this dict/list structure for each input/ouput in the for-loops below
xfp_dict[xfp_hex][base_path["base_path"]] = hd_pubkey_obj
Expand Down
4 changes: 3 additions & 1 deletion buidl/test/test_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,9 @@ def test_mixed_slip132_p2wsh_sortedmulti(self):
"account_index": 0,
},
]
p2wsh_sortedmulti_obj = P2WSHSortedMulti(quorum_m, key_records)
p2wsh_sortedmulti_obj = P2WSHSortedMulti(
quorum_m, key_records, sort_key_records=False
)
# notice that both values are tpub, despite ingesting Vpub
want = "wsh(sortedmulti(1,[aa917e75/48h/1h/0h/2h]tpubDEZRP2dRKoGRJnR9zn6EoLouYKbYyjFsxywgG7wMQwCDVkwNvoLhcX1rTQipYajmTAF82kJoKDiNCgD4wUPahACE7n1trMSm7QS8B3S1fdy/0/*,[2553c4b8/48h/1h/0h/2h/2046266013/1945465733/1801020214/1402692941]tpubDNVvpMhdGTmQg1AT6muju2eUWPXWWAtUSyc1EQ2MxJ2s97fMqFZQbpzQM4gU8bwzfFM7KBpSXRJ5v2Wu8sY2GF5ZpXm3qy8GLArZZNM1Wru/0/*))#0lfdttke"
self.assertEqual(str(p2wsh_sortedmulti_obj), want)
Expand Down
8 changes: 4 additions & 4 deletions buidl/test/test_psbt_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@ def test_sweep_1of2_p2sh(self):
"e0c595c5": [
{
# action x12
"xpub_hex": "tpubDBnspiLZfrq1V7j1iuMxGiPsuHyy6e4QBnADwRrbH89AcnsUEMfWiAYXmSbMuNFsrMdnbQRDGGSM1AFGL6zUWNVSmwRavoJzdQBbZKLgLgd",
"xpub_b58": "tpubDBnspiLZfrq1V7j1iuMxGiPsuHyy6e4QBnADwRrbH89AcnsUEMfWiAYXmSbMuNFsrMdnbQRDGGSM1AFGL6zUWNVSmwRavoJzdQBbZKLgLgd",
"base_path": "m/45h/0",
}
],
"838f3ff9": [
{
# agent x12
"xpub_hex": "tpubDAKJicb9Tkw34PFLEBUcbnH99twN3augmg7oYHHx9Aa9iodXmA4wtGEJr8h2XjJYqn2j1v5qHLjpWEe8aPihmC6jmsgomsuc9Zeh4ZushNk",
"xpub_b58": "tpubDAKJicb9Tkw34PFLEBUcbnH99twN3augmg7oYHHx9Aa9iodXmA4wtGEJr8h2XjJYqn2j1v5qHLjpWEe8aPihmC6jmsgomsuc9Zeh4ZushNk",
"base_path": "m/45h/0",
}
],
Expand Down Expand Up @@ -141,14 +141,14 @@ def test_spend_1of2_with_change(self):
"e0c595c5": [
{
# action x12
"xpub_hex": "tpubDBnspiLZfrq1V7j1iuMxGiPsuHyy6e4QBnADwRrbH89AcnsUEMfWiAYXmSbMuNFsrMdnbQRDGGSM1AFGL6zUWNVSmwRavoJzdQBbZKLgLgd",
"xpub_b58": "tpubDBnspiLZfrq1V7j1iuMxGiPsuHyy6e4QBnADwRrbH89AcnsUEMfWiAYXmSbMuNFsrMdnbQRDGGSM1AFGL6zUWNVSmwRavoJzdQBbZKLgLgd",
"base_path": "m/45h/0",
}
],
"838f3ff9": [
{
# agent x12
"xpub_hex": "tpubDAKJicb9Tkw34PFLEBUcbnH99twN3augmg7oYHHx9Aa9iodXmA4wtGEJr8h2XjJYqn2j1v5qHLjpWEe8aPihmC6jmsgomsuc9Zeh4ZushNk",
"xpub_b58": "tpubDAKJicb9Tkw34PFLEBUcbnH99twN3augmg7oYHHx9Aa9iodXmA4wtGEJr8h2XjJYqn2j1v5qHLjpWEe8aPihmC6jmsgomsuc9Zeh4ZushNk",
"base_path": "m/45h/0",
}
],
Expand Down
Loading

0 comments on commit 6c3a0ad

Please sign in to comment.