bip388 import/export

This commit is contained in:
scgbckbone 2025-06-30 18:07:46 +02:00
parent 0b20ef5360
commit 37a677e6f9
7 changed files with 242 additions and 33 deletions

View File

@ -1451,7 +1451,8 @@ class NewMiniscriptEnrollRequest(UserAuthorizedAction):
self.pop_menu()
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None, miniscript=False):
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None,
miniscript=False):
# Offer to import (enroll) a new multisig/miniscript wallet. Allow reject by user.
from glob import dis
from multisig import MultisigWallet
@ -1461,6 +1462,7 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_
dis.fullscreen('Wait...')
dis.busy_bar(True)
bip388 = False
try:
if sf_len:
with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd:
@ -1468,9 +1470,14 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_
try:
j_conf = ujson.loads(config)
assert "desc" in j_conf, "'desc' key required"
config = j_conf["desc"]
assert config, "'desc' empty"
if "desc_template" in j_conf and "keys_info" in j_conf:
assert "name" in j_conf
config = j_conf
bip388 = miniscript = True
else:
assert "desc" in j_conf, "'desc' key required"
config = j_conf["desc"]
assert config, "'desc' empty"
if "name" in j_conf:
# name from json has preference over filenames and desc checksum
@ -1488,7 +1495,7 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_
msc = MiniScriptWallet.from_file(config, name=name)
elif miniscript:
msc = MiniScriptWallet.from_file(config, name=name)
msc = MiniScriptWallet.from_file(config, name=name, bip388=bip388)
else:
msc = MultisigWallet.from_file(config, name=name)

View File

@ -1047,7 +1047,7 @@ async def bsms_signer_round2(menu, label, item):
nodes = []
progress_counter = 0.2 # last displayed progress
# (desired value after loop - last displayed progress) / N
progress_chunk = (0.5 - progress_counter) / len(desc_obj.miniscript.keys)
progress_chunk = (0.5 - progress_counter) / len(desc_obj.keys)
for key in desc_obj.keys:
if key.origin.cc_fp == my_xfp:
my_keys.append(key)

View File

@ -169,6 +169,9 @@ class KeyDerivationInfo:
def not_hardened(x):
assert (b"'" not in x) and (b"h" not in x), "Cannot use hardened sub derivation path"
def get_ext_int(self):
return self.indexes[self.multi_path_index]
@classmethod
def parse(cls, s):
err = "Malformed key derivation"
@ -183,6 +186,7 @@ class KeyDerivationInfo:
cls.not_hardened(ext_num)
int_num, char = read_until(s, b">")
assert char, err
assert b";" not in int_num, "Solved cardinality > 2"
cls.not_hardened(int_num)
assert int_num != ext_num # cannot be the same
@ -241,12 +245,11 @@ class Key:
self.chain_type = chain_type
def __eq__(self, other):
return self.origin == other.origin \
and self.derivation.indexes == other.derivation.indexes
return hash(self) == hash(other)
def __hash__(self):
# return hash(self.to_string())
return hash(self.origin) + hash(self.derivation)
return hash(self.node.pubkey()) + hash(self.derivation)
def __len__(self):
return 34 - int(self.taproot) # <33:sec> or <32:xonly>
@ -422,4 +425,29 @@ def bip388_wallet_policy_to_descriptor(desc_tmplt, keys_info):
k_str = keys_info[i]
ph = "@%d" % i
desc_tmplt = desc_tmplt.replace(ph, k_str)
return desc_tmplt
return desc_tmplt.replace("/**", "/<0;1>/*")
def bip388_validate_policy(desc_tmplt, keys_info):
from uio import BytesIO
s = BytesIO(desc_tmplt)
r = []
while True:
got, char = read_until(s, b"@")
if not char:
# no more - done
break
# key derivation info required for policy
got, char = read_until(s, b"/")
assert char, "key derivation missing"
num = int(got.decode())
if num not in r:
r.append(num)
assert s.read(1) in b"<*", "need multipath"
assert len(r) == len(keys_info), "Invalid policy"
assert r == list(range(len(r))), "Out of order"

View File

@ -144,21 +144,29 @@ class Descriptor:
assert len(self.miniscript.keys) == len(set(self.miniscript.keys)), "Insane"
my_xfp = settings.get('xfp')
ext_nums = set()
int_nums = set()
for k in self.keys:
has_mine += k.validate(my_xfp)
ext, int = k.derivation.get_ext_int()
ext_nums.add(ext)
int_nums.add(int)
c += 1
assert ext_nums.isdisjoint(int_nums), "Non-disjoint multipath"
assert c <= max_signers, "max signers"
assert has_mine > 0, 'My key %s missing in descriptor.' % xfp2str(my_xfp).upper()
def bip388_wallet_policy(self):
# only same origin keys
keys_info = OrderedDict()
for k in self.keys:
if k.origin not in keys_info:
keys_info[k.origin] = k.to_string(subderiv=False)
pk = k.node.pubkey()
if pk not in keys_info:
keys_info[pk] = k.to_string(subderiv=False)
desc_tmplt = self.to_string(checksum=False)
desc_tmplt = self.to_string(checksum=False).replace("/<0;1>/*", "/**")
keys_info = list(keys_info.values())
for i, k_str in enumerate(keys_info):
@ -218,13 +226,16 @@ class Descriptor:
if self.tapscript:
# internal is always first
# otherwise order of keys is not preserved (after set ops)
keys = set()
# use ordered dict as order preserving set
keys = OrderedDict()
# add internal key
keys[self.key] = None
# taptree keys
for lv in self.tapscript.iter_leaves():
for k in lv.keys:
keys.add(k)
keys[k] = None
self._keys = [self.key] + list(keys)
self._keys = list(keys)
elif self.miniscript:
self._keys = self.miniscript.keys

View File

@ -6,7 +6,7 @@ import ngu, ujson, uio, chains, ure, version, stash
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from serializations import ser_compact_size, ser_string
from desc_utils import Key, read_until, bip388_wallet_policy_to_descriptor, append_checksum
from desc_utils import Key, read_until, bip388_wallet_policy_to_descriptor, append_checksum, bip388_validate_policy
from public_constants import MAX_TR_SIGNERS, AF_P2TR
from wallet import BaseStorageWallet, MAX_BIP32_IDX
from menu import MenuSystem, MenuItem
@ -23,7 +23,7 @@ class MiniScriptWallet(BaseStorageWallet):
key_name = "miniscript"
def __init__(self, name, desc_tmplt=None, keys_info=None, desc=None,
af=None, ik_u=None, chain=None):
af=None, ik_u=None):
assert (desc_tmplt and keys_info) or desc
@ -51,7 +51,7 @@ class MiniScriptWallet(BaseStorageWallet):
rv.storage_idx = idx
return rv
def to_descriptor(self):
def to_descriptor(self, validate=False):
if self.desc is None:
# actual descriptor is not loaded, but was asked for
# fill policy - aka storage format - to actual descriptor
@ -66,7 +66,7 @@ class MiniScriptWallet(BaseStorageWallet):
desc_str = bip388_wallet_policy_to_descriptor(self.desc_tmplt, self.keys_info)
print("loading... filled policy:\n", desc_str)
# no need to validate already saved descriptor - was validated upon enroll
self.desc = Descriptor.from_string(desc_str, validate=False)
self.desc = Descriptor.from_string(desc_str, validate=validate)
# cache len always 1
glob.DESC_CACHE = {}
glob.DESC_CACHE[self.name] = self.desc
@ -191,23 +191,37 @@ class MiniScriptWallet(BaseStorageWallet):
await ux_show_story(msg)
@classmethod
def from_file(cls, config, name=None):
def from_bip388_wallet_policy(cls, name, desc_template, keys_info):
bip388_validate_policy(desc_template, keys_info)
msc = cls(name, desc_template, keys_info)
msc.to_descriptor(validate=True)
return msc
@classmethod
def from_file(cls, config, name=None, bip388=False):
from descriptor import Descriptor
if name is None:
desc_obj, cs = Descriptor.from_string(config.strip(), checksum=True)
name = cs
if bip388:
# config is JSON wallet policy
wal = cls.from_bip388_wallet_policy(config["name"], config["desc_template"],
config["keys_info"])
else:
name = to_ascii_printable(name)
desc_obj = Descriptor.from_string(config.strip())
if name is None:
desc_obj, cs = Descriptor.from_string(config.strip(), checksum=True)
name = cs
else:
name = to_ascii_printable(name)
desc_obj = Descriptor.from_string(config.strip())
wal = cls(name, desc=desc_obj)
wal = cls(name, desc=desc_obj)
# BIP388 wasn't generated yet - generating from descriptor upon import/enroll
wal.desc_tmplt, wal.keys_info = desc_obj.bip388_wallet_policy()
# BIP388 wasn't generated yet - generating from descriptor upon import/enroll
wal.desc_tmplt, wal.keys_info = desc_obj.bip388_wallet_policy()
wal.ik_u = desc_obj.key and desc_obj.key.is_provably_unspendable
wal.addr_fmt = desc_obj.addr_fmt
bip388_validate_policy(wal.desc_tmplt, wal.keys_info)
wal.ik_u = wal.desc.key and wal.desc.key.is_provably_unspendable
wal.addr_fmt = wal.desc.addr_fmt
return wal
def find_duplicates(self):
@ -337,7 +351,7 @@ class MiniScriptWallet(BaseStorageWallet):
name = "BIP-388 Wallet Policy"
fname_pattern = 'b388-%s.json' % self.name
res = ujson.dumps({"name": self.name,
"desc_tmplt": self.desc_tmplt.replace("/<0;1>/*", "/**"),
"desc_template": self.desc_tmplt.replace("/<0;1>/*", "/**"),
"keys_info": self.keys_info})
else:
name = "Miniscript"

View File

@ -0,0 +1,145 @@
# (c) Copyright 2025 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# BIP-0388 vectors https://github.com/bitcoin/bips/blob/master/bip-0388.mediawiki
valid = [
(
"pkh(@0/**)",
["[6738736c/44'/0'/0']xpub6Br37sWxruYfT8ASpCjVHKGwgdnYFEn98DwiN76i2oyY6fgH1LAPmmDcF46xjxJr22gw4jmVjTE2E3URMnRPEPYyo1zoPSUba563ESMXCeb"],
"pkh([6738736c/44'/0'/0']xpub6Br37sWxruYfT8ASpCjVHKGwgdnYFEn98DwiN76i2oyY6fgH1LAPmmDcF46xjxJr22gw4jmVjTE2E3URMnRPEPYyo1zoPSUba563ESMXCeb/<0;1>/*)",
),
(
"sh(wpkh(@0/**))",
["[6738736c/49'/0'/1']xpub6Bex1CHWGXNNwGVKHLqNC7kcV348FxkCxpZXyCWp1k27kin8sRPayjZUKDjyQeZzGUdyeAj2emoW5zStFFUAHRgd5w8iVVbLgZ7PmjAKAm9"],
"sh(wpkh([6738736c/49'/0'/1']xpub6Bex1CHWGXNNwGVKHLqNC7kcV348FxkCxpZXyCWp1k27kin8sRPayjZUKDjyQeZzGUdyeAj2emoW5zStFFUAHRgd5w8iVVbLgZ7PmjAKAm9/<0;1>/*))",
),
(
"wpkh(@0/**)",
["[6738736c/84'/0'/2']xpub6CRQzb8u9dmMcq5XAwwRn9gcoYCjndJkhKgD11WKzbVGd932UmrExWFxCAvRnDN3ez6ZujLmMvmLBaSWdfWVn75L83Qxu1qSX4fJNrJg2Gt"],
"wpkh([6738736c/84'/0'/2']xpub6CRQzb8u9dmMcq5XAwwRn9gcoYCjndJkhKgD11WKzbVGd932UmrExWFxCAvRnDN3ez6ZujLmMvmLBaSWdfWVn75L83Qxu1qSX4fJNrJg2Gt/<0;1>/*)",
),
(
"tr(@0/**)",
["[6738736c/86'/0'/0']xpub6CryUDWPS28eR2cDyojB8G354izmx294BdjeSvH469Ty3o2E6Tq5VjBJCn8rWBgesvTJnyXNAJ3QpLFGuNwqFXNt3gn612raffLWfdHNkYL"],
"tr([6738736c/86'/0'/0']xpub6CryUDWPS28eR2cDyojB8G354izmx294BdjeSvH469Ty3o2E6Tq5VjBJCn8rWBgesvTJnyXNAJ3QpLFGuNwqFXNt3gn612raffLWfdHNkYL/<0;1>/*)",
),
(
"wsh(sortedmulti(2,@0/**,@1/**))",
["[6738736c/48'/0'/0'/2']xpub6FC1fXFP1GXLX5TKtcjHGT4q89SDRehkQLtbKJ2PzWcvbBHtyDsJPLtpLtkGqYNYZdVVAjRQ5kug9CsapegmmeRutpP7PW4u4wVF9JfkDhw",
"[b2b1f0cf/48'/0'/0'/2']xpub6EWhjpPa6FqrcaPBuGBZRJVjzGJ1ZsMygRF26RwN932Vfkn1gyCiTbECVitBjRCkexEvetLdiqzTcYimmzYxyR1BZ79KNevgt61PDcukmC7"],
"wsh(sortedmulti(2,[6738736c/48'/0'/0'/2']xpub6FC1fXFP1GXLX5TKtcjHGT4q89SDRehkQLtbKJ2PzWcvbBHtyDsJPLtpLtkGqYNYZdVVAjRQ5kug9CsapegmmeRutpP7PW4u4wVF9JfkDhw/<0;1>/*,[b2b1f0cf/48'/0'/0'/2']xpub6EWhjpPa6FqrcaPBuGBZRJVjzGJ1ZsMygRF26RwN932Vfkn1gyCiTbECVitBjRCkexEvetLdiqzTcYimmzYxyR1BZ79KNevgt61PDcukmC7/<0;1>/*))",
),
(
"wsh(thresh(3,pk(@0/**),s:pk(@1/**),s:pk(@2/**),sln:older(12960)))",
["[6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa",
"[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js",
"[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2"],
"wsh(thresh(3,pk([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<0;1>/*),s:pk([b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js/<0;1>/*),s:pk([a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2/<0;1>/*),sln:older(12960)))",
),
(
"wsh(or_d(pk(@0/**),and_v(v:multi(2,@1/**,@2/**,@3/**),older(65535))))",
["[6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa",
"[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js",
"[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2",
"[bb641298/44'/0'/0'/100']xpub6Dz8PHFmXkYkykQ83ySkruky567XtJb9N69uXScJZqweYiQn6FyieajdiyjCvWzRZ2GoLHMRE1cwDfuJZ6461YvNRGVBJNnLA35cZrQKSRJ"],
"wsh(or_d(pk([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<0;1>/*),and_v(v:multi(2,[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js/<0;1>/*,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2/<0;1>/*,[bb641298/44'/0'/0'/100']xpub6Dz8PHFmXkYkykQ83ySkruky567XtJb9N69uXScJZqweYiQn6FyieajdiyjCvWzRZ2GoLHMRE1cwDfuJZ6461YvNRGVBJNnLA35cZrQKSRJ/<0;1>/*),older(65535))))",
),
(
"tr(@0/**,{sortedmulti_a(1,@0/<2;3>/*,@1/**),or_b(pk(@2/**),s:pk(@3/**))})",
["[6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa",
"xpub6Fc2TRaCWNgfT49nRGG2G78d1dPnjhW66gEXi7oYZML7qEFN8e21b2DLDipTZZnfV6V7ivrMkvh4VbnHY2ChHTS9qM3XVLJiAgcfagYQk6K",
"xpub6GxHB9kRdFfTqYka8tgtX9Gh3Td3A9XS8uakUGVcJ9NGZ1uLrGZrRVr67DjpMNCHprZmVmceFTY4X4wWfksy8nVwPiNvzJ5pjLxzPtpnfEM",
"xpub6GjFUVVYewLj5no5uoNKCWuyWhQ1rKGvV8DgXBG9Uc6DvAKxt2dhrj1EZFrTNB5qxAoBkVW3wF8uCS3q1ri9fueAa6y7heFTcf27Q4gyeh6"],
"tr([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<0;1>/*,{sortedmulti_a(1,[6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa/<2;3>/*,xpub6Fc2TRaCWNgfT49nRGG2G78d1dPnjhW66gEXi7oYZML7qEFN8e21b2DLDipTZZnfV6V7ivrMkvh4VbnHY2ChHTS9qM3XVLJiAgcfagYQk6K/<0;1>/*),or_b(pk(xpub6GxHB9kRdFfTqYka8tgtX9Gh3Td3A9XS8uakUGVcJ9NGZ1uLrGZrRVr67DjpMNCHprZmVmceFTY4X4wWfksy8nVwPiNvzJ5pjLxzPtpnfEM/<0;1>/*),s:pk(xpub6GjFUVVYewLj5no5uoNKCWuyWhQ1rKGvV8DgXBG9Uc6DvAKxt2dhrj1EZFrTNB5qxAoBkVW3wF8uCS3q1ri9fueAa6y7heFTcf27Q4gyeh6/<0;1>/*))})",
),
# (
# "tr(musig(@0,@1,@2)/**,{and_v(v:pk(musig(@0,@1)/**),older(12960)),{and_v(v:pk(musig(@0,@2)/**),older(12960)),and_v(v:pk(musig(@1,@2)/**),older(12960))}})",
# ["[6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa",
# "[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js",
# "[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2"],
# "tr(musig([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa,[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2)/<0;1>/*,{and_v(v:pk(musig([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa,[b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js)/<0;1>/*),older(12960)),{and_v(v:pk(musig([6738736c/48'/0'/0'/100']xpub6FC1fXFP1GXQpyRFfSE1vzzySqs3Vg63bzimYLeqtNUYbzA87kMNTcuy9ubr7MmavGRjW2FRYHP4WGKjwutbf1ghgkUW9H7e3ceaPLRcVwa,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2)/<0;1>/*),older(12960)),and_v(v:pk(musig([b2b1f0cf/44'/0'/0'/100']xpub6EYajCJHe2CK53RLVXrN14uWoEttZgrRSaRztujsXg7yRhGtHmLBt9ot9Pd5ugfwWEu6eWyJYKSshyvZFKDXiNbBcoK42KRZbxwjRQpm5Js,[a666a867/44'/0'/0'/100']xpub6Dgsze3ujLi1EiHoCtHFMS9VLS1UheVqxrHGfP7sBJ2DBfChEUHV4MDwmxAXR2ayeytpwm3zJEU3H3pjCR6q6U5sP2p2qzAD71x9z5QShK2)/<0;1>/*),older(12960))}})",
# ),
]
invalid = [
(
# Key placeholder with no path following it
"key derivation missing",
"pkh(@0)",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
),
(
# Key placeholder with an explicit path present
"need multipath",
"pkh(@0/0/**)",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
),
(
# Key placeholders out of order
"Out of order",
"sh(multi(1,@1/**,@0/**))",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg",
"[6738736c/44'/0'/0']xpub6Br37sWxruYfT8ASpCjVHKGwgdnYFEn98DwiN76i2oyY6fgH1LAPmmDcF46xjxJr22gw4jmVjTE2E3URMnRPEPYyo1zoPSUba563ESMXCeb"],
),
(
# Skipped key placeholder @1
"Out of order",
"sh(multi(1,@0/**,@2/**))",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg",
"[6738736c/44'/0'/0']xpub6Br37sWxruYfT8ASpCjVHKGwgdnYFEn98DwiN76i2oyY6fgH1LAPmmDcF46xjxJr22gw4jmVjTE2E3URMnRPEPYyo1zoPSUba563ESMXCeb"],
),
(
# Repeated keys with the same path expression
"Insane",
"sh(multi(1,@0/**,@0/**))",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
),
(
# Non-disjoint multipath expressions (@0/1/* appears twice)
"Non-disjoint multipath",
"sh(multi(1,@0/<0;1>/*,@0/<1;2>/*))",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
),
(
# Taproot non-disjoint multipath expressions (@0/1/* appears twice in tapscript)
"Non-disjoint multipath",
"tr(@0/<5;6>/*,multi_a(1,@0/<0;1>/*,@0/<1;2>/*))",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
),
(
# Taproot non-disjoint multipath expressions (@0/1/* appears twice as internal key and tapscript key)
"Non-disjoint multipath",
"tr(@0/<0;1>/*,multi_a(1,@0/<5;6>/*,@0/<1;2>/*))",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
),
(
# solved cardinality > 2
"Solved cardinality > 2",
"pkh(@0/<0;1;2>/*)",
["[0f056943/99h/0h/0h]xpub6DMjVrmtVxXyn5hBuLScBtHeQ9X3ws6uasj7mWRu7ay7mQrX5suQKwYgNZBJYnWKugRk1KrgmTHtgwGvB7QcgELYCuacE3oA25SGMQZTiRg"],
)
]
import glob
from glob import settings
from descriptor import Descriptor
from miniscript import MiniScriptWallet
settings.set('chain', "BTC")
# valid vectors
for policy, keys_info, desc in valid:
d = Descriptor.from_string(desc, validate=False)
pol, ki = d.bip388_wallet_policy()
assert pol == policy, "\n" + pol + "\n" + policy
assert keys_info == keys_info
# invalid vectors
for err, policy, keys_info in invalid:
glob.DESC_CACHE = {}
try:
msc = MiniScriptWallet.from_bip388_wallet_policy("random_name", policy, keys_info)
assert False, "succeeded, but must have failed!"
except BaseException as e:
if err not in str(e):
raise

View File

@ -394,4 +394,8 @@ def test_script(sim_execfile):
res = sim_execfile('devtest/unit_script.py')
assert res == ""
def test_bip388(sim_execfile):
res = sim_execfile('devtest/unit_bip388.py')
assert res == ""
# EOF