diff --git a/docs/taproot.md b/docs/taproot.md index b45841fa..62d1bc10 100644 --- a/docs/taproot.md +++ b/docs/taproot.md @@ -25,21 +25,31 @@ MUST be generated with above-mentoned methods to be considered change. ## Provably unspendable internal key -There are few methods to provide/generate provably unspendable internal key, if users wish to only use script path -for multisig. +There are few methods to provide/generate provably unspendable internal key, if users wish to only use tapscript script path. -1. use provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs). This way is leaking the information that key path spending is not possible and therefore not recommended privacy-wise. +1. **(recommended)** Origin-less extended key serialization with H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs) as BIP-32 key and random chaincode. + + `tr(xpub/<0:1>/*, sortedmulti_a(2,@0,@1))` which is the same thing as `tr(xpub, sortedmulti_a(2,@0,@1))` because `/<0;1>/*` is implied if not derivation path not provided. + +2. **(recommended)** Use `unspend(` [notation](https://gist.github.com/sipa/06c5c844df155d4e5044c2c8cac9c05e#unspendable-keys). Has to be ranged. + + `tr(unspend(77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76)/<0:1>/*, sortedmulti_a(2,@0,@1))` + +3. use **static** provably unspendable internal key H from [BIP-0341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs). `tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0, sortedmulti_a(2,@0,@1))` -2. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable. +4. use COLDCARD specific placeholder `@` to let HWW pick a fresh integer r in the range 0...n-1 uniformly at random and use `H + rG` as internal key. COLDCARD will not store r and therefore user is not able to prove to other party how the key was generated and whether it is actually unspendable. `tr(r=@, sortedmulti_a(MofN))` -3. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created. +5. pick a fresh integer r in the range 0...n-1 uniformly at random yourself and provide that in the descriptor. COLDCARD generates internal key with `H + rG`. It is possible to prove to other party that this internal key does not have a known discrete logarithm with respect to G by revealing r to a verifier who can then reconstruct how the internal key was created. `tr(r=77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76, sortedmulti_a(2,@0,@1))` +Option 3. leaks the information that key path spending is not possible and therefore is not recommended privacy-wise. +Options 4. and 5. are problematic to some extent as internal key is static. Use recommended options 1. and 2. if the fact that internal key is unspendable should remain private. + ## Limitations diff --git a/releases/EdgeChangeLog.md b/releases/EdgeChangeLog.md new file mode 100644 index 00000000..4175fa17 --- /dev/null +++ b/releases/EdgeChangeLog.md @@ -0,0 +1,44 @@ +# Change Log + +## Warning: Edge Version + +```diff +- This preview version of firmware has not yet been qualified +- and tested to the same standard as normal Coinkite products. +- It is recommended only for developers and early adopters +- for experimental use. DO NOT use for large Bitcoin amounts. +``` + +This lists the changes in the most recent EDGE firmware, for each hardware platform. + +# Shared Improvements - Both Mk4 and Q + +- New Feature: Ranged provably unspendable keys and `unspend(` support for Taproot descriptors +- New Feature: Address ownership for miniscript and tapscript wallets +- Enhancement: Address explorer simplified UI for tapscript addresses +- Bugfix: Constant `AFC_BECH32M` incorrectly set `AFC_WRAPPED` and `AFC_BECH32`. +- Bugfix: Trying to set custom URL for NFC push transaction caused yikes + + +# Mk4 Specific Changes + +## 5.3.3X - 2024-07-04 + +- Bugfix: Fix yikes displaying BIP-85 WIF when both NFC and VDisk are OFF +- Bugfix: Fix inability to export change addresses when both NFC and Vdisk id OFF +- Bugfix: In BIP-39 words menu, show space character rather than Nokia-style placeholder + which could be confused for an underscore. + + +# Q Specific Changes + +## 1.2.3QX - 2024-07-04 + +- Enhancement: Miniscript and (BB)Qr codes +- Bugfix: Properly clear LCD screen after simple QR code is shown + + + +# Release History + +- [`History-Edge.md`](History-Edge.md) diff --git a/releases/History-Edge.md b/releases/History-Edge.md new file mode 100644 index 00000000..29d1d6a5 --- /dev/null +++ b/releases/History-Edge.md @@ -0,0 +1,54 @@ +## Warning: Edge Version + +```diff +- This preview version of firmware has not yet been qualified +- and tested to the same standard as normal Coinkite products. +- It is recommended only for developers and early adopters +- for experimental use. DO NOT use for large Bitcoin amounts. +``` + +## 6.3.3 + +## 6.2.2X - 2024-01-18 + +- New Feature: Miniscript [USB interface](https://github.com/Coldcard/ckcc-protocol/blob/master/README.md#miniscript) +- New Feature: Named miniscript imports. Wrap descriptor in json + `{"name:"n0", "desc":""}` with `name` key to use this name instead of the + filename. Mostly usefull for USB and NFC imports that have no file, in which case name + was created from descriptor checksum. +- Enhancement: Allow keys with same origin, differentiated only by change index derivation + in miniscript descriptor. +- Enhancement: HSM `wallet` rule enabled for miniscript +- Enhancement: Add `msas` in to the `share_addrs` HSM [rule](https://coldcard.com/docs/hsm/rules/) + to be able to check miniscript addresses in HSM mode. +- Enhancement: HW Accelerated AES CTR for BSMS and passphrase saver +- Bugfix: Do not allow to import duplicate miniscript + wallets (thanks to [AnchorWatch](https://www.anchorwatch.com/)) +- Bugfix: Saving passphrase on SD Card caused a freeze that required reboot + +## 6.2.1X - 2023-10-26 + +- New Feature: Enroll Miniscript wallet via USB (requires ckcc `v1.4.0`) +- New Feature: Temporary Seed from COLDCARD encrypted backup +- Enhancement: Add current temporary seed to Seed Vault from within Seed Vault menu. + If current active temporary seed is not saved yet, `Add current tmp` menu item is + present in Seed Vault menu. +- Reorg: `12 Words` menu option preferred on the top of the menu in all the seed menus +- Enhancement: Mainnet/Testnet separation. Only show wallets for current active chain. +- contains all the changes from the newest stable `5.2.0-mk4` firmware + +## 6.1.0X - 2023-06-20 + +- New Feature: Miniscript and MiniTapscript support (`docs/miniscript.md`) +- Enhancement: Tapscript up to 8 leafs +- Address explorer display refined slightly (cosmetic) + +## 6.0.0X - 2023-05-12 + +- New Feature: Taproot keyspend & Tapscript multisig `sortedmulti_a` (tree depth = 0) +- New Feature: Support BIP-0129 Bitcoin Secure Multisig Setup (BSMS). + Both Coordinator and Signer roles are supported. +- Enhancement: change Key Origin Information export format in multisig `addresses.csv` according to [BIP-0380](https://github.com/bitcoin/bips/blob/master/bip-0380.mediawiki#key-expressions) + `(m=0F056943)/m/48'/1'/0'/2'/0/0` --> `[0F056943/48'/1'/0'/2'/0/0]` +- Bugfix: correct `scriptPubkey` parsing for segwit v1-v16 +- Bugfix: do not infer segwit just by availability of `PSBT_IN_WITNESS_UTXO` in PSBT \ No newline at end of file diff --git a/shared/address_explorer.py b/shared/address_explorer.py index d0b0b370..2e765839 100644 --- a/shared/address_explorer.py +++ b/shared/address_explorer.py @@ -288,7 +288,6 @@ Press (3) if you really understand and accept these risks. if ms_wallet: msg, addrs = ms_wallet.make_addresses_msg(msg, start, n, change) - else: # single-signer wallets from wallet import MasterSingleSigWallet @@ -305,8 +304,7 @@ Press (3) if you really understand and accept these risks. # export options k0 = 'to show change addresses' if allow_change and change == 0 else None export_msg, escape = export_prompt_builder('address summary file', - no_qr=bool(ms_wallet), key0=k0, - force_prompt=True) + key0=k0, force_prompt=True) if version.has_qwerty: escape += KEY_LEFT+KEY_RIGHT+KEY_HOME+KEY_PAGE_UP+KEY_PAGE_DOWN+KEY_QR else: diff --git a/shared/bsms.py b/shared/bsms.py index df6e2031..84666462 100644 --- a/shared/bsms.py +++ b/shared/bsms.py @@ -723,7 +723,7 @@ async def bsms_coordinator_round2(menu, label, item): prompt, escape = export_prompt_builder(title) if prompt: ch = await ux_show_story(prompt, escape=escape) - if ch == KEY_NFC if version_mod.has_qwerty else '3': + if ch == (KEY_NFC if version_mod.has_qwerty else '3'): if et == "2": for i, token in enumerate(tokens): ch = await ux_show_story("Exporting data for co-signer #%d with token %s" @@ -922,7 +922,7 @@ async def bsms_signer_round1(*a): prompt, escape = export_prompt_builder(title) if prompt: ch = await ux_show_story(prompt, escape=escape) - if ch == KEY_NFC if version.has_qwerty else '3': + if ch == (KEY_NFC if version.has_qwerty else '3'): force_vdisk = None if isinstance(result_data, bytes): result_data = b2a_hex(result_data).decode() @@ -986,7 +986,7 @@ async def bsms_signer_round2(menu, label, item): if prompt: ch = await ux_show_story(prompt, escape=escape) - if ch == KEY_NFC if version.has_qwerty else '3': + if ch == (KEY_NFC if version.has_qwerty else '3'): force_vdisk = None desc_template_data = await NFC.read_bsms_data() diff --git a/shared/desc_utils.py b/shared/desc_utils.py index 8a198a4f..4e48a2e0 100644 --- a/shared/desc_utils.py +++ b/shared/desc_utils.py @@ -2,7 +2,7 @@ # # Copyright (c) 2020 Stepan Snigirev MIT License embit/arguments.py # -import ngu, chains +import ngu, chains, ustruct from io import BytesIO from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_CLASSIC, AF_P2TR from binascii import unhexlify as a2b_hex @@ -12,7 +12,7 @@ from serializations import ser_compact_size WILDCARD = "*" -PROVABLY_UNSPENDABLE = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" +PROVABLY_UNSPENDABLE = b'\x02P\x92\x9bt\xc1\xa0IT\xb7\x8bK`5\xe9z^\x07\x8aZ\x0f(\xec\x96\xd5G\xbf\xee\x9a\xce\x80:\xc0' INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" @@ -90,7 +90,7 @@ def multisig_descriptor_template(xpub, path, xfp, addr_fmt): descriptor_template = "sh(sortedmulti(M,%s,...))" elif addr_fmt == AF_P2TR: # provably unspendable BIP-0341 - descriptor_template = "tr(" + PROVABLY_UNSPENDABLE + ",sortedmulti_a(M,%s,...))" + descriptor_template = "tr(" + b2a_hex(PROVABLY_UNSPENDABLE[1:]).decode() + ",sortedmulti_a(M,%s,...))" else: return None descriptor_template = descriptor_template % key_exp @@ -249,20 +249,13 @@ class Key: self.derivation = derivation self.taproot = taproot self.chain_type = chain_type - if not isinstance(self.node, bytes): - assert self.origin, "Key origin info is required" def __eq__(self, other): - return self.origin.psbt_derivation() == other.origin.psbt_derivation() \ + return self.origin == other.origin \ and self.derivation.indexes == other.derivation.indexes def __hash__(self): - orig = tuple(self.origin.psbt_derivation()) - der = self.derivation.indexes.copy() - if self.derivation.multi_path_index is not None: - der[self.derivation.multi_path_index] = tuple(der[self.derivation.multi_path_index]) - der = tuple(der) - return hash(orig+der) + return hash(self.to_string()) def __len__(self): return 34 - int(self.taproot) # <33:sec> or <32:xonly> @@ -282,6 +275,10 @@ class Key: def parse(cls, s): first = s.read(1) origin = None + if first == b"u": + s.seek(-1, 1) + return Unspend.parse(s) + if first == b"[": prefix, char = read_until(s, b"]") if char != b"]": @@ -324,13 +321,7 @@ class Key: node.deserialize(key_str) else: # only unspendable keys can be bare pubkeys - for now - # TODO - # if b"unspend(" in key_str: - # node = ngu.hdnode.HDNode() - # chain_code = key_str.replace(b"unspend(", b"").replace(b")", b"") - # node.chaincode = a2b_hex(chain_code) - # node.pubkey = a2b_hex("02" + PROVABLY_UNSPENDABLE) - H = a2b_hex(PROVABLY_UNSPENDABLE) + H = PROVABLY_UNSPENDABLE[1:] if b"r=" in key_str: _, r = key_str.split(b"=") if r == b"@": @@ -381,9 +372,10 @@ class Key: if self.origin: origin = KeyOriginInfo(self.origin.fingerprint, self.origin.derivation + [idx]) else: - origin = KeyOriginInfo(self.node.my_fp(), [idx]) - # empty derivation - derivation = None + fp = ustruct.pack('") + if char is None: + raise ValueError("Failed reading the key, missing >") + der += branch + b">" + rest, char = read_until(s, b",)") + der += rest + if char is not None: + s.seek(-1, 1) + + node = ngu.hdnode.HDNode().from_chaincode_pubkey(chain_code, + PROVABLY_UNSPENDABLE) + der = KeyDerivationInfo.from_string(der.decode()) + return cls(node, None, der, chain_type=None) + + def to_string(self, external=True, internal=True, subderiv=True): + res = "unspend(%s)" % b2a_hex(self.node.chain_code()).decode() + if self.derivation and subderiv: + res += "/" + self.derivation.to_string(external, internal) + + return res + + @property + def is_provably_unspendable(self): + return True + + def fill_policy(policy, keys, external=True, internal=True): keys_len = len(keys) for i in range(keys_len - 1, -1, -1): @@ -460,7 +503,7 @@ def fill_policy(policy, keys, external=True, internal=True): # subderivation is part of the policy subderiv = False x = ix + ph_len - substr = policy[x:x+26] # 26 is longest possible subderivation allowed "/<2147483647;2147483646>/*" + substr = policy[x:x+26] # 26 is the longest possible subderivation allowed "/<2147483647;2147483646>/*" mp_start = substr.find("<") assert mp_start != -1 mp_end = substr.find(">") diff --git a/shared/descriptor.py b/shared/descriptor.py index 9d65175d..988818aa 100644 --- a/shared/descriptor.py +++ b/shared/descriptor.py @@ -6,11 +6,11 @@ import ngu, chains from io import BytesIO from collections import OrderedDict from binascii import hexlify as b2a_hex -from utils import cleanup_deriv_path, check_xpub, xfp2str +from utils import cleanup_deriv_path, check_xpub, xfp2str, swab32 from public_constants import AF_CLASSIC, AF_P2WPKH, AF_P2WPKH_P2SH, AF_P2TR from public_constants import AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, MAX_SIGNERS, MAX_TR_SIGNERS from desc_utils import parse_desc_str, append_checksum, descriptor_checksum, Key -from desc_utils import taproot_tree_helper, fill_policy +from desc_utils import taproot_tree_helper, fill_policy, Unspend from miniscript import Miniscript @@ -232,7 +232,7 @@ class Descriptor: if self.tapscript: assert len(self.keys) <= MAX_TR_SIGNERS assert self.key # internal key (would fail during parse) - if not isinstance(self.key.node, bytes): + if not self.key.is_provably_unspendable: to_check += [self.key] else: assert self.key is None and self.miniscript, "not miniscript" @@ -282,16 +282,19 @@ class Descriptor: return 25 # OP_DUP OP_HASH160 <20:pkh> OP_EQUALVERIFY OP_CHECKSIG def xfp_paths(self): - keys = self.keys - if self.taproot and self.key.origin: - # ignore provably unspendable - keys += [self.key] + res = [] + if self.taproot: + if self.key.origin: + # spendable internal key + res.append(self.key.origin.psbt_derivation()) + elif not isinstance(self.key.node, bytes): + if self.key.is_provably_unspendable: + res.append([swab32(self.key.node.my_fp())]) - return [ - key.origin.psbt_derivation() - for key in keys - if key.origin - ] + for k in self.keys: + if k.origin: + res.append(k.origin.psbt_derivation()) + return res @property def is_wrapped(self): @@ -505,7 +508,7 @@ class Descriptor: @classmethod def read_from(cls, s, taproot=False): - start = s.read(7) + start = s.read(8) sh = False wsh = False wpkh = False @@ -515,8 +518,8 @@ class Descriptor: if start.startswith(b"tr("): is_miniscript = False # miniscript vs. tapscript (that can contain miniscripts in tree) taproot = True - s.seek(-4, 1) - internal_key = Key.parse(s) # internal key is a must + s.seek(-5, 1) + internal_key = Key.parse(s) # internal key is a must - also handles unspend( internal_key.taproot = True sep = s.read(1) if sep == b")": @@ -527,26 +530,26 @@ class Descriptor: elif start.startswith(b"sh(wsh("): sh = True wsh = True + s.seek(-1, 1) elif start.startswith(b"wsh("): sh = False wsh = True - s.seek(-3, 1) - elif start.startswith(b"sh(wpkh"): + s.seek(-4, 1) + elif start.startswith(b"sh(wpkh("): is_miniscript = False sh = True wpkh = True - assert s.read(1) == b"(" elif start.startswith(b"wpkh("): is_miniscript = False wpkh = True - s.seek(-2, 1) + s.seek(-3, 1) elif start.startswith(b"pkh("): is_miniscript = False - s.seek(-3, 1) + s.seek(-4, 1) elif start.startswith(b"sh("): sh = True wsh = False - s.seek(-4, 1) + s.seek(-5, 1) else: raise ValueError("Invalid descriptor") @@ -603,65 +606,14 @@ class Descriptor: # this will become legacy one day # instead use <0;1> descriptor format res = [] - for external, internal in [(True, False), (False, True)]: + for external in (True, False): desc_obj = { - "desc": self.to_string(external, internal), + "desc": self.to_string(external, not external), "active": True, "timestamp": "now", - "internal": internal, + "internal": not external, "range": [0, 100], } res.append(desc_obj) return res - - def pretty_serialize(self): - # TODO not enabled - """Serialize in pretty and human-readable format""" - inner_ident = 1 - res = "# Coldcard descriptor export\n" - res += "# order of keys in the descriptor does not matter, will be sorted before creating script (BIP-67)\n" - if self.addr_fmt == AF_P2SH: - res += "# bare multisig - p2sh\n" - res += "sh(sortedmulti(\n%s\n))" - # native segwit - elif self.addr_fmt == AF_P2WSH: - res += "# native segwit - p2wsh\n" - res += "wsh(sortedmulti(\n%s\n))" - - # wrapped segwit - elif self.addr_fmt == AF_P2WSH_P2SH: - res += "# wrapped segwit - p2sh-p2wsh\n" - res += "sh(wsh(sortedmulti(\n%s\n)))" - - elif self.addr_fmt == AF_P2TR: - inner_ident = 2 - res += "# taproot multisig - p2tr\n" - res += "tr(\n" - if isinstance(self.internal_key, str): - res += "\t" + "# internal key (provably unspendable)\n" - res += "\t" + self.internal_key + ",\n" - res += "\t" + "sortedmulti_a(\n%s\n))" - else: - ik_ser = self.serialize_keys(keys=[self.internal_key])[0] - res += "\t" + "# internal key\n" - res += "\t" + ik_ser + ",\n" - res += "\t" + "sortedmulti_a(\n%s\n))" - else: - raise ValueError("Malformed descriptor") - - assert len(self.keys) == self.N - inner = ("\t" * inner_ident) + "# %d of %d (%s)\n" % ( - self.M, self.N, - "requires all participants to sign" if self.M == self.N else "threshold") - inner += ("\t" * inner_ident) + str(self.M) + ",\n" - ser_keys = self.serialize_keys() - for i, key_str in enumerate(ser_keys, start=1): - if i == self.N: - inner += ("\t" * inner_ident) + key_str - else: - inner += ("\t" * inner_ident) + key_str + ",\n" - - checksum = self.serialize().split("#")[1] - - return (res % inner) + "#" + checksum \ No newline at end of file diff --git a/shared/miniscript.py b/shared/miniscript.py index 607b7bd1..e1f6595d 100644 --- a/shared/miniscript.py +++ b/shared/miniscript.py @@ -13,7 +13,7 @@ from wallet import BaseStorageWallet from menu import MenuSystem, MenuItem from ux import ux_show_story, ux_confirm, ux_dramatic_pause from files import CardSlot, CardMissingError, needs_microsd -from utils import problem_file_line, xfp2str, addr_fmt_label, truncate_address, to_ascii_printable +from utils import problem_file_line, xfp2str, addr_fmt_label, truncate_address, to_ascii_printable, swab32 from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC, KEY_ENTER @@ -158,6 +158,10 @@ class MiniScriptWallet(BaseStorageWallet): ik = Key.from_string(self.key) if ik.origin: res.append(ik.origin.psbt_derivation()) + elif not isinstance(ik.node, bytes): + if ik.is_provably_unspendable: + res.append([swab32(ik.node.my_fp())]) + for k in self.keys: k = Key.from_string(k) if k.origin: @@ -232,14 +236,14 @@ class MiniScriptWallet(BaseStorageWallet): def ux_policy(self): if self.taproot and self.policy: - return "Taproot tree keys:\n\n" + self.policy + return "Tapscript:\n\n" + self.policy return self.policy - async def _detail(self, new_wallet=False, is_duplicate=False): + async def _detail(self, new_wallet=False, is_duplicate=False, short=False): s = addr_fmt_label(self.addr_fmt) + "\n\n" if self.taproot: - s += self.taproot_internal_key_detail() + s += self.taproot_internal_key_detail(short=short) s += self.ux_policy() @@ -248,7 +252,7 @@ class MiniScriptWallet(BaseStorageWallet): story += ", OK to approve, X to cancel." return story - async def show_detail(self, new_wallet=False, duplicates=None): + async def show_detail(self, new_wallet=False, duplicates=None, short=False): title = self.name story = "" if duplicates: @@ -257,7 +261,7 @@ class MiniScriptWallet(BaseStorageWallet): elif new_wallet: title = None story += "Create new miniscript wallet?\n\nWallet Name:\n %s\n\n" % self.name - story += await self._detail(new_wallet, is_duplicate=duplicates) + story += await self._detail(new_wallet, is_duplicate=duplicates, short=short) while True: ch = await ux_show_story(story, title=title, escape="1") if ch == "1": @@ -268,13 +272,24 @@ class MiniScriptWallet(BaseStorageWallet): else: return True - def taproot_internal_key_detail(self): + def taproot_internal_key_detail(self, short=False): if self.taproot: key = Key.from_string(self.key) s = "Taproot internal key:\n\n" if key.is_provably_unspendable: - unspend = b2a_hex(key.node).decode() - s += "%s (provably unspendable)\n\n" % unspend + note = "provably unspendable" + if short: + s += note + else: + if isinstance(key.node, bytes): + s += b2a_hex(key.node).decode() + s += "\n (%s)" % note + else: + s += self.key + if type(key) is Key: + # it is unspendable, BUT not unspend( + s += "\n (%s)" % note + s += "\n\n" else: xfp, deriv, xpub = key.to_cc_data() s += '%s:\n %s\n\n%s/%s\n\n' % (xfp2str(xfp), deriv, xpub, @@ -373,14 +388,14 @@ class MiniScriptWallet(BaseStorageWallet): if d.tapscript: yield (idx, addr, - [str(k.origin) for k in d.keys], + ["[%s]" % str(k.origin) for k in d.keys], script, d.key.serialize(), str(d.key.origin) if d.key.origin else "") else: yield (idx, addr, - [str(k.origin) for k in d.keys], + ["[%s]" % str(k.origin) for k in d.keys], script, None, None) @@ -393,40 +408,39 @@ class MiniScriptWallet(BaseStorageWallet): addrs = [] - for i, addr, paths, _, ik, ikp in self.yield_addresses(start, n, - change=bool(change), - scripts=False): - if i == 0 and ik: - ik = b2a_hex(ik).decode() - msg += "Taproot internal key:\n\n" - if ikp: - msg += ikp + "\n" + ik + "\n\n" - else: - msg += '%s (provably unspendable)\n\n' % ik - - if len(paths) <= 4: - msg += "Taproot tree keys:\n\n" - - if i == 0 and len(paths) <= 4 and not ik: + for idx, addr, paths, _, ik, _ in self.yield_addresses(start, n, + change=bool(change), + scripts=False): + if idx == 0 and len(paths) <= 4 and not ik: msg += '\n'.join(paths) + '\n =>\n' else: change_idx = set([int(p.split("/")[-2]) for p in paths]) if len(change_idx) == 1: - msg += '.../%d/%d =>\n' % (list(change_idx)[0], i) + msg += '.../%d/%d =>\n' % (list(change_idx)[0], idx) else: - msg += '.../%d =>\n' % i + msg += '.../%d =>\n' % idx addrs.append(addr) msg += truncate_address(addr) + '\n\n' - dis.progress_bar_show(i / n) + dis.progress_sofar(idx - start + 1, n) return msg, addrs def generate_address_csv(self, start, n, change): - scr_h = "Taptree" if self.desc.taproot else "Script" + part = [] + if self.taproot: + scr_h = "Taptree" + if self.desc.key.is_provably_unspendable: + part = ["Unspendable Internal Key"] + else: + part = ["Internal Key"] + + else: + scr_h = "Script" + yield '"' + '","'.join( ['Index', 'Payment Address', scr_h] + ['Derivation'] * len(self.keys) - + (["Internal Key"] if self.taproot else []) + + part ) + '"\n' for (idx, addr, derivs, script, ik, ikp) in self.yield_addresses(start, n, change=bool(change)): @@ -434,7 +448,10 @@ class MiniScriptWallet(BaseStorageWallet): ln += '","'.join(derivs) if ik: # internal xonly key with its derivation (if any) - ln += '","%s' % (ikp + b2a_hex(ik).decode()) + if ikp: + ln += '","[%s]%s' % (ikp, b2a_hex(ik).decode()) + else: + ln += '","%s' % (b2a_hex(ik).decode()) ln += '"\n' yield ln @@ -443,20 +460,28 @@ class MiniScriptWallet(BaseStorageWallet): # this will become legacy one day # instead use <0;1> descriptor format res = [] - for external, internal in [(True, False), (False, True)]: + for external in (True, False): desc_obj = { - "desc": self.to_string(external, internal), + "desc": self.to_string(external, not external, unspend_compat=True), "active": True, "timestamp": "now", - "internal": internal, + "internal": not external, "range": [0, 100], } res.append(desc_obj) return res - def to_string(self, external=True, internal=True, checksum=True): + def to_string(self, external=True, internal=True, checksum=True, unspend_compat=False): if self._key: key = self._key + if "unspend(" in key and unspend_compat: + # for bitcoin core that does not support 'unspend(' descriptor notation + # serialize 'unspend(' as classic extended key + k = Key.from_string(self.key) + key = k.extended_public_key() + if k.derivation: + key += "/" + k.derivation.to_string(external, internal) + multipath_rgx = ure.compile(r"<\d+;\d+>") match = multipath_rgx.search(key) if match: @@ -508,7 +533,7 @@ class MiniScriptWallet(BaseStorageWallet): fname_pattern = fname_pattern + ".txt" if core: - msg = "importdescriptor cmd" + msg = "importdescriptors cmd" dis.fullscreen('Wait...') core_obj = self.bitcoin_core_serialize() core_str = ujson.dumps(core_obj) @@ -605,7 +630,7 @@ async def miniscript_wallet_detail(menu, label, item): msc = item.arg - return await msc.show_detail() + return await msc.show_detail(short=True) async def import_miniscript(*a): # pick text file from SD card, import as multisig setup file @@ -851,6 +876,9 @@ class Miniscript: # cannot have same keys in single miniscript forbiden = (Sortedmulti_a, Multi_a) keys = self.keys + # provably unspendable taproot internal key is not covered here + # all other keys (miniscript,tapscript) require key origin info + assert all(k.origin for k in keys), "Key origin info is required" assert len(keys) == len(set(keys)), "Insane" if taproot: forbiden = (Sortedmulti, Multi) diff --git a/shared/psbt.py b/shared/psbt.py index 0d8e7b0b..75d2cf60 100644 --- a/shared/psbt.py +++ b/shared/psbt.py @@ -5,7 +5,7 @@ from ustruct import unpack_from, unpack, pack from ubinascii import hexlify as b2a_hex from utils import xfp2str, B2A, keypath_to_str, validate_derivation_path_length -from utils import seconds2human_readable, datetime_from_timestamp, datetime_to_str +from utils import seconds2human_readable, datetime_from_timestamp, datetime_to_str, problem_file_line import stash, gc, history, sys, ngu, ckcc, chains from uhashlib import sha256 from uio import BytesIO @@ -2245,7 +2245,9 @@ class psbtObject(psbtProxy): if inp.taproot_subpaths: # this can be set to False even if we haev script ready, but can send keypath # tapscript schnorrsig = True - xfp_paths = [item[1:] for item in inp.taproot_subpaths.values() if item[0]] + # previously internal keys would be filtered here with if item[0] + # as per BIP-371 first item is leaf hashes which has to be empty for internal key + xfp_paths = [item[1:] for item in inp.taproot_subpaths.values()] int_path = inp.taproot_subpaths[which_key][1:] skp = keypath_to_str(int_path) else: diff --git a/testing/bip32.py b/testing/bip32.py index e09cf087..d52867dc 100644 --- a/testing/bip32.py +++ b/testing/bip32.py @@ -737,6 +737,12 @@ class BIP32Node: ek = PubKeyNode.parse(extended_key, testnet) return cls(ek, netcode="XTN" if testnet else "BTC") + @classmethod + def from_chaincode_pubkey(cls, chain_code, pubkey, netcode="XTN"): + node = PubKeyNode(pubkey, chain_code, 0, 0, + False if netcode == "BTC" else True) + return cls(node, netcode=netcode) + def subkey_for_path(self, path): path_list = str_to_path(path) node = self.node diff --git a/testing/conftest.py b/testing/conftest.py index 077bf643..3541d932 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1701,7 +1701,7 @@ def load_shared_mod(): return doit @pytest.fixture -def verify_detached_signature_file(microsd_path, virtdisk_path): +def verify_detached_signature_file(microsd_path, virtdisk_path, garbage_collector): def doit(fnames, sig_fname, way, addr_fmt=None): fpaths = [] for fname in fnames: @@ -1710,6 +1710,7 @@ def verify_detached_signature_file(microsd_path, virtdisk_path): else: path = virtdisk_path(fname) fpaths.append(path) + garbage_collector.append(path) if way == "sd": sig_path = microsd_path(sig_fname) @@ -1750,9 +1751,7 @@ def verify_detached_signature_file(microsd_path, virtdisk_path): assert (hashlib.sha256(contents).digest().hex() + fn_addendum) in msg assert verify_message(address, sig, msg) is True - try: - os.unlink(sig_path) - except: pass + garbage_collector.append(sig_path) return fcontents[0], address return doit @@ -1786,7 +1785,7 @@ def load_export_and_verify_signature(microsd_path, virtdisk_path, verify_detache @pytest.fixture def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_text, nfc_read_json, load_export_and_verify_signature, is_q1, press_cancel, press_select, readback_bbqr, - cap_screen_qr): + cap_screen_qr, garbage_collector): def doit(way, label, is_json, sig_check=True, addr_fmt=AF_CLASSIC, ret_sig_addr=False, tail_check=None, sd_key=None, vdisk_key=None, nfc_key=None, ret_fname=False, fpattern=None, qr_key=None): @@ -1877,6 +1876,8 @@ def load_export(need_keypress, cap_story, microsd_path, virtdisk_path, nfc_read_ if is_json: export = json.loads(export) + garbage_collector.append(path) + press_select() if ret_sig_addr and sig_addr: @@ -1921,7 +1922,7 @@ def tapsigner_encrypted_backup(microsd_path, virtdisk_path): return doit @pytest.fixture -def choose_by_word_length(need_keypress): +def choose_by_word_length(need_keypress, press_select): # for use in seed XOR menu system def doit(num_words): if num_words == 12: @@ -1929,7 +1930,7 @@ def choose_by_word_length(need_keypress): elif num_words == 18: need_keypress("2") else: - need_keypress("y") + press_select() return doit # workaround: need these fixtures to be global so I can call test from a test @@ -2163,6 +2164,9 @@ def txout_explorer(cap_story, press_cancel, need_keypress, is_q1): elif af in ("p2wpkh", "p2wsh"): target = "bc1q" if chain == "BTC" else "tb1q" assert addr.startswith(target) + elif af == "p2tr": + target = "bc1p" if chain == "BTC" else "tb1p" + assert addr.startswith(target) elif af in ("p2sh", "p2wpkh-p2sh", "p2wsh-p2sh"): target = "3" if chain == "BTC" else "2" assert addr.startswith(target) @@ -2276,6 +2280,15 @@ def dev_core_import_object(dev): return descriptors +@pytest.fixture +def garbage_collector(): + to_remove = [] + yield to_remove + for pth in to_remove: + try: + os.remove(pth) + except: pass + # useful fixtures from test_backup import backup_system from test_bbqr import readback_bbqr, render_bbqr, readback_bbqr_ll diff --git a/testing/test_bsms.py b/testing/test_bsms.py index b6aa08e5..6a296a32 100644 --- a/testing/test_bsms.py +++ b/testing/test_bsms.py @@ -269,11 +269,16 @@ def make_coordinator_round2(make_coordinator_round1, settings_get, settings_set, @pytest.mark.parametrize("encryption_type", ["1", "2", "3"]) @pytest.mark.parametrize("M_N", [(2,2), (3, 5), (15, 15)]) @pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh"]) -def test_coordinator_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_home, need_keypress, pick_menu_item, - cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, virtdisk_path, - settings_get, virtdisk_wipe, microsd_wipe, press_select, is_q1): +def test_coordinator_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_home, need_keypress, + pick_menu_item, cap_menu, cap_story, microsd_path, settings_remove, + nfc_read_text, request, settings_get, microsd_wipe, press_select, is_q1): + if way == "vdisk": + virtdisk_wipe = request.getfixturevalue("virtdisk_wipe") + virtdisk_path = request.getfixturevalue("virtdisk_path") + virtdisk_wipe() + M, N = M_N - virtdisk_wipe() + microsd_wipe() settings_remove(BSMS_SETTINGS) # clear bsms goto_home() @@ -419,11 +424,15 @@ def test_coordinator_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_ @pytest.mark.parametrize("M_N", [(2,2), (3, 5), (15, 15)]) @pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh"]) def test_signer_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_home, need_keypress, pick_menu_item, cap_menu, - cap_story, microsd_path, settings_remove, nfc_read_text, virtdisk_path, settings_get, - make_coordinator_round1, nfc_write_text, virtdisk_wipe, microsd_wipe, press_select, + cap_story, microsd_path, settings_remove, nfc_read_text, request, settings_get, + make_coordinator_round1, nfc_write_text, microsd_wipe, press_select, is_q1): + if way == "vdisk": + virtdisk_wipe = request.getfixturevalue("virtdisk_wipe") + virtdisk_path = request.getfixturevalue("virtdisk_path") + virtdisk_wipe() + M, N = M_N - virtdisk_wipe() microsd_wipe() tokens = make_coordinator_round1(M, N, addr_fmt, encryption_type, way) if encryption_type != "3": @@ -572,9 +581,9 @@ def test_signer_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_home, @pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh"]) @pytest.mark.parametrize("auto_collect", [True, False]) def test_coordinator_round2(way, encryption_type, M_N, addr_fmt, auto_collect, clear_ms, goto_home, need_keypress, - cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, virtdisk_path, + cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, request, settings_get, make_coordinator_round1, make_signer_round1, nfc_write_text, - virtdisk_wipe, microsd_wipe, pick_menu_item, press_select, is_q1): + microsd_wipe, pick_menu_item, press_select, is_q1): def get_token(index): if len(tokens) == 1 and encryption_type == "1": token = tokens[0] @@ -584,8 +593,12 @@ def test_coordinator_round2(way, encryption_type, M_N, addr_fmt, auto_collect, c token = "00" return token + if way == "vdisk": + virtdisk_wipe = request.getfixturevalue("virtdisk_wipe") + virtdisk_path = request.getfixturevalue("virtdisk_path") + virtdisk_wipe() + M, N = M_N - virtdisk_wipe() microsd_wipe() tokens = make_coordinator_round1(M, N, addr_fmt, encryption_type, way=way, tokens_only=True) all_data = [] @@ -793,12 +806,15 @@ def test_coordinator_round2(way, encryption_type, M_N, addr_fmt, auto_collect, c @pytest.mark.parametrize("M_N", [(2,2), (3, 5), (15, 15)]) @pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh"]) def test_signer_round2(refuse, way, encryption_type, M_N, addr_fmt, clear_ms, goto_home, need_keypress, pick_menu_item, - cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, virtdisk_path, settings_get, - make_coordinator_round2, nfc_write_text, virtdisk_wipe, microsd_wipe, with_checksum, + cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, request, settings_get, + make_coordinator_round2, nfc_write_text, microsd_wipe, with_checksum, press_select, press_cancel, is_q1): + if way == "vdisk": + virtdisk_wipe = request.getfixturevalue("virtdisk_wipe") + virtdisk_path = request.getfixturevalue("virtdisk_path") + virtdisk_wipe() M, N = M_N clear_ms() - virtdisk_wipe() microsd_wipe() desc_template, token = make_coordinator_round2(M, N, addr_fmt, encryption_type, way=way, add_checksum=with_checksum) goto_home() @@ -950,9 +966,8 @@ def test_invalid_token_signer_round1(token, way, pick_menu_item, cap_story, need @pytest.mark.parametrize("failure", ["slip", "wrong_sig", "bsms_version"]) @pytest.mark.parametrize("encryption_type", ["1", "2", "3"]) def test_failure_coordinator_round2(encryption_type, make_coordinator_round1, make_signer_round1, microsd_wipe, cap_menu, - virtdisk_wipe, pick_menu_item, press_select, goto_home, cap_story, failure, + pick_menu_item, press_select, goto_home, cap_story, failure, need_keypress): - virtdisk_wipe() microsd_wipe() def get_token(index): @@ -1025,7 +1040,7 @@ def test_failure_coordinator_round2(encryption_type, make_coordinator_round1, ma # TODO do this for NFC too when length requirements are lifted from 250 @pytest.mark.parametrize("encryption_type", ["1", "2"]) def test_wrong_encryption_coordinator_round2(encryption_type, make_coordinator_round1, make_signer_round1, microsd_wipe, - cap_menu, virtdisk_wipe, pick_menu_item, need_keypress, goto_home, cap_story, + cap_menu, pick_menu_item, need_keypress, goto_home, cap_story, press_cancel, press_select): def get_token(index): if len(tokens) == 1 and encryption_type == "1": @@ -1036,7 +1051,6 @@ def test_wrong_encryption_coordinator_round2(encryption_type, make_coordinator_r token = "00" return token - virtdisk_wipe() microsd_wipe() tokens = make_coordinator_round1(2, 2, "p2wsh", encryption_type, way="sd", tokens_only=True) for i in range(2): @@ -1103,8 +1117,7 @@ def test_wrong_encryption_coordinator_round2(encryption_type, make_coordinator_r @pytest.mark.parametrize("encryption_type", ["1", "2", "3"]) def test_failure_signer_round2(encryption_type, goto_home, press_select, pick_menu_item, cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, virtdisk_path, settings_get, microsd_wipe, - make_coordinator_round2, virtdisk_wipe, failure, need_keypress): - virtdisk_wipe() + make_coordinator_round2, failure, need_keypress): microsd_wipe() if failure == "wrong_address": kws = {failure: True} diff --git a/testing/test_miniscript.py b/testing/test_miniscript.py index 5032504d..3ae30c56 100644 --- a/testing/test_miniscript.py +++ b/testing/test_miniscript.py @@ -6,8 +6,9 @@ import pytest, json, time, itertools, struct, random, os from ckcc.protocol import CCProtocolPacker from constants import AF_P2TR from psbt import BasicPSBT -from charcodes import KEY_QR, KEY_NFC, KEY_RIGHT, KEY_CANCEL +from charcodes import KEY_QR, KEY_RIGHT, KEY_CANCEL from bbqr import split_qrs +from bip32 import BIP32Node H = "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" # BIP-0341 @@ -25,6 +26,14 @@ TREE = { } +def ranged_unspendable_internal_key(chain_code=32 * b"\x01", subderiv="/<0;1>/*"): + # provide ranged provably unspendable key in serialized extended key format for core to understand it + # core does NOT understand 'unspend(' + pk = b"\x02" + bytes.fromhex(H) + node = BIP32Node.from_chaincode_pubkey(chain_code, pk) + return node.hwif() + subderiv + + @pytest.fixture def offer_minsc_import(cap_story, dev): def doit(config, allow_non_ascii=False): @@ -43,7 +52,7 @@ def offer_minsc_import(cap_story, dev): @pytest.fixture def import_miniscript(goto_home, pick_menu_item, cap_story, need_keypress, - nfc_write_text, press_select, scan_a_qr): + nfc_write_text, press_select, scan_a_qr, press_nfc): def doit(fname, way="sd", data=None): goto_home() pick_menu_item('Settings') @@ -55,7 +64,7 @@ def import_miniscript(goto_home, pick_menu_item, cap_story, need_keypress, if "via NFC" not in story: pytest.skip("nfc disabled") - need_keypress(KEY_NFC) + press_nfc() time.sleep(.1) if isinstance(data, dict): data = json.dumps(data) @@ -111,6 +120,7 @@ def import_duplicate(import_miniscript, press_cancel, virtdisk_path, microsd_pat if way == "vdisk": path_f = virtdisk_path + time.sleep(.2) title, story = import_miniscript(fname, way, data=data) if "unique names" in story: # trying to import duplicate with same name @@ -129,6 +139,7 @@ def import_duplicate(import_miniscript, press_cancel, virtdisk_path, microsd_pat f.write(res) title, story = import_miniscript(new_fname, way, data=data) + time.sleep(.2) assert "duplicate of already saved wallet" in story assert "OK to approve" not in story @@ -141,8 +152,11 @@ def import_duplicate(import_miniscript, press_cancel, virtdisk_path, microsd_pat @pytest.fixture def miniscript_descriptors(goto_home, pick_menu_item, need_keypress, cap_story, - microsd_path, is_q1, readback_bbqr, cap_screen_qr): + microsd_path, is_q1, readback_bbqr, cap_screen_qr, + garbage_collector): + def doit(minsc_name): + qr_external = None goto_home() pick_menu_item("Settings") pick_menu_item("Miniscript") @@ -150,6 +164,7 @@ def miniscript_descriptors(goto_home, pick_menu_item, need_keypress, cap_story, pick_menu_item("Descriptors") pick_menu_item("Export") need_keypress("1") # internal and external separately + time.sleep(.1) if is_q1: # check QR need_keypress(KEY_QR) @@ -163,9 +178,10 @@ def miniscript_descriptors(goto_home, pick_menu_item, need_keypress, cap_story, qr_external, qr_internal = data.split("\n") need_keypress(KEY_CANCEL) - pick_menu_item("Export") - need_keypress("1") # internal and external separately - time.sleep(.2) + pick_menu_item("Export") + need_keypress("1") # internal and external separately + time.sleep(.2) + title, story = cap_story() if "Press (1)" in story: need_keypress("1") @@ -174,13 +190,16 @@ def miniscript_descriptors(goto_home, pick_menu_item, need_keypress, cap_story, assert "Miniscript file written" in story fname = story.split("\n\n")[-1] - with open(microsd_path(fname), "r") as f: + fpath = microsd_path(fname) + garbage_collector.append(fpath) + with open(fpath, "r") as f: cont = f.read() external, internal = cont.split("\n") if qr_external: assert qr_external == external assert qr_internal == internal return external, internal + return doit @@ -265,10 +284,7 @@ def address_explorer_check(goto_home, pick_menu_item, need_keypress, cap_menu, pick_menu_item(wal_name) title, story = cap_story() - if addr_fmt == "bech32m": - assert "Taproot internal key" in story - else: - assert "Taproot internal key" not in story + assert "Taproot internal key" not in story if way == "qr": need_keypress(KEY_QR) @@ -281,14 +297,13 @@ def address_explorer_check(goto_home, pick_menu_item, need_keypress, cap_menu, else: contents = load_export(way, label="Address summary", is_json=False, sig_check=False) addr_cont = contents.strip() - # time.sleep(5) time.sleep(.5) title, story = cap_story() assert "(0)" in story assert "change addresses." in story need_keypress("0") - time.sleep(5) + time.sleep(.5) title, story = cap_story() assert "(0)" not in story assert "change addresses." not in story @@ -320,7 +335,10 @@ def address_explorer_check(goto_home, pick_menu_item, need_keypress, cap_menu, cc_addrs_split_change = addr_cont_change.split("\n") # header is different for taproot if addr_fmt == "bech32m": - assert "Internal Key" in cc_addrs_split[0] + try: + assert "Internal Key" in cc_addrs_split[0] + except AssertionError: + assert "Unspendable Internal Key" in cc_addrs_split[0] assert "Taptree" in cc_addrs_split[0] else: assert "Internal Key" not in cc_addrs_split[0] @@ -343,6 +361,26 @@ def address_explorer_check(goto_home, pick_menu_item, need_keypress, cap_menu, if export_check: cc_external, cc_internal = miniscript_descriptors(cc_minsc_name) + + unspend = "unspend(" + if unspend in cc_external: + assert "unspend(" in cc_internal + netcode = "XTN" if "tpub" in cc_external else "BTC" + # bitcoin core does not recognize unspend( - needs hack + # CC properly exports any imported unspend( for bitcoin core + # as extended key serialization xpub/<0;1>/* + start_idx = cc_external.find(unspend) + assert start_idx != -1 + end_idx = start_idx + len(unspend) + 64 + 1 + uns = cc_external[start_idx: end_idx] + chain_code = bytes.fromhex(uns[len(unspend):-1]) + node = BIP32Node.from_chaincode_pubkey(chain_code, + b"\x02" + bytes.fromhex(H), + netcode=netcode) + ek = node.hwif() + cc_external = cc_external.replace(uns, ek) + cc_internal = cc_internal.replace(uns, ek) + assert cc_external.split("#")[0] == external_desc.split("#")[0].replace("'", "h") assert cc_internal.split("#")[0] == internal_desc.split("#")[0].replace("'", "h") @@ -400,7 +438,8 @@ def test_liana_miniscripts_simple(addr_fmt, recovery, lt_type, minisc, clear_min use_regtest, bitcoind, microsd_wipe, load_export, dev, address_explorer_check, get_cc_key, import_miniscript, bitcoin_core_signer, import_duplicate, press_select, - virtdisk_path): + virtdisk_path, skip_if_useless_way, garbage_collector): + skip_if_useless_way(way) normal_cosign_core = False recovery_cosign_core = False if "multi(" in minisc.split("),", 1)[0]: @@ -445,6 +484,7 @@ def test_liana_miniscripts_simple(addr_fmt, recovery, lt_type, minisc, clear_min use_regtest() clear_miniscript() + goto_home() name = "core-miniscript" fname = f"{name}.txt" if way in ["qr", "nfc"]: @@ -453,11 +493,12 @@ def test_liana_miniscripts_simple(addr_fmt, recovery, lt_type, minisc, clear_min path_f = microsd_path if way == "sd" else virtdisk_path data = None fpath = path_f(fname) + garbage_collector.append(fpath) with open(fpath, "w") as f: f.write(desc) wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True, - passphrase=None, avoid_reuse=False, descriptors=True) + passphrase=None, avoid_reuse=False, descriptors=True) _, story = import_miniscript(fname, way=way, data=data) try: @@ -506,8 +547,10 @@ def test_liana_miniscripts_simple(addr_fmt, recovery, lt_type, minisc, clear_min psbt = signer1.walletprocesspsbt(psbt, True, "ALL")["psbt"] name = f"{name}.psbt" - with open(microsd_path(name), "w") as f: + fpath = microsd_path(name) + with open(fpath, "w") as f: f.write(psbt) + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -527,8 +570,10 @@ def test_liana_miniscripts_simple(addr_fmt, recovery, lt_type, minisc, clear_min press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() + garbage_collector.append(fpath_psbt) # with open(microsd_path(fname_txn), "r") as f: # final_txn = f.read().strip() res = wo.finalizepsbt(final_psbt) @@ -563,9 +608,12 @@ def test_liana_miniscripts_complex(addr_fmt, minsc, bitcoind, use_regtest, clear microsd_path, pick_menu_item, cap_story, load_export, goto_home, address_explorer_check, cap_menu, get_cc_key, import_miniscript, bitcoin_core_signer, - import_duplicate, press_select, way): + import_duplicate, press_select, way, skip_if_useless_way, + garbage_collector): + skip_if_useless_way(way) use_regtest() clear_miniscript() + goto_home() minsc, to_gen = minsc signer_keys = minsc.count("@") @@ -617,6 +665,8 @@ def test_liana_miniscripts_complex(addr_fmt, minsc, bitcoind, use_regtest, clear with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) + wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) _, story = import_miniscript(fname, way=way, data=data) @@ -663,8 +713,10 @@ def test_liana_miniscripts_complex(addr_fmt, minsc, bitcoind, use_regtest, clear psbt = s.walletprocesspsbt(psbt, True, "ALL")["psbt"] pname = f"{name}.psbt" - with open(microsd_path(pname), "w") as f: + ppath = microsd_path(pname) + with open(ppath, "w") as f: f.write(psbt) + garbage_collector.append(ppath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -684,8 +736,10 @@ def test_liana_miniscripts_complex(addr_fmt, minsc, bitcoind, use_regtest, clear press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() + garbage_collector.append(fpath_psbt) # with open(microsd_path(fname_txn), "r") as f: # final_txn = f.read().strip() res = wo.finalizepsbt(final_psbt) @@ -714,7 +768,7 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export, pick_menu_item, goto_home, cap_menu, microsd_path, use_regtest, get_cc_key, import_miniscript, bitcoin_core_signer, import_duplicate, press_select, - virtdisk_path): + virtdisk_path, garbage_collector): def doit(M, N, script_type, internal_key=None, cc_account=0, funded=True, r=None, tapscript_threshold=False, add_own_pk=False, same_account=False, way="sd"): @@ -811,8 +865,10 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export, data = None fname = f"{name}.txt" path_f = microsd_path if way == 'sd' else virtdisk_path - with open(path_f(fname), "w") as f: + fpath = path_f(fname) + with open(fpath, "w") as f: f.write(desc + "\n") + garbage_collector.append(fpath) else: data = dict(name=name, desc=desc) @@ -821,7 +877,7 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export, assert name in story if script_type == "p2tr": assert "Taproot internal key" in story - assert "Taproot tree keys" in story + assert "Tapscript" in story assert "Press (1) to see extended public keys" in story if script_type == "p2wsh": assert "P2WSH" in story @@ -903,18 +959,21 @@ def bitcoind_miniscript(bitcoind, need_keypress, cap_story, load_export, @pytest.mark.bitcoind @pytest.mark.parametrize("cc_first", [True, False]) @pytest.mark.parametrize("add_pk", [True, False]) -@pytest.mark.parametrize("same_acct", [True, False]) +@pytest.mark.parametrize("same_acct", [None, True, False]) @pytest.mark.parametrize("way", ["qr", "sd"]) @pytest.mark.parametrize("M_N", [(3,4),(4,5),(5,6)]) def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, pick_menu_item, cap_menu, cap_story, microsd_path, use_regtest, bitcoind, microsd_wipe, load_export, bitcoind_miniscript, add_pk, same_acct, get_cc_key, - press_select, way): + press_select, way, skip_if_useless_way, garbage_collector): + skip_if_useless_way(way) M, N = M_N clear_miniscript() microsd_wipe() internal_key = None - if same_acct: + if same_acct is None: + internal_key = ranged_unspendable_internal_key() + elif same_acct: # provide internal key with same account derivation (change based derivation) internal_key = get_cc_key("m/86h/1h/0h", subderiv='/<10;11>/*') @@ -929,8 +988,12 @@ def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, pick_menu_item, if not cc_first: for s in signers[0:M-1]: psbt = s.walletprocesspsbt(psbt, True, "DEFAULT")["psbt"] - with open(microsd_path("ts_tree.psbt"), "w") as f: + + psbt_fpath = microsd_path("ts_tree.psbt") + with open(psbt_fpath, "w") as f: f.write(psbt) + + garbage_collector.append(psbt_fpath) time.sleep(2) goto_home() pick_menu_item("Ready To Sign") @@ -947,8 +1010,10 @@ def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, pick_menu_item, title, story = cap_story() assert title == "PSBT Signed" fname = [i for i in story.split("\n\n") if ".psbt" in i][0] - with open(microsd_path(fname), "r") as f: + fpath = microsd_path(fname) + with open(fpath, "r") as f: psbt = f.read().strip() + garbage_collector.append(fpath) if cc_first: # we MUST be able to finalize this without anyone else if add pk if not add_pk: @@ -967,14 +1032,23 @@ def test_tapscript(M_N, cc_first, clear_miniscript, goto_home, pick_menu_item, @pytest.mark.parametrize("add_pk", [True, False]) @pytest.mark.parametrize('M_N', [(3, 15), (2, 2), (3, 5)]) @pytest.mark.parametrize('way', ["qr", "sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('internal_type', ["unspend(", "xpub", "static"]) def test_bitcoind_tapscript_address(M_N, clear_miniscript, bitcoind_miniscript, use_regtest, way, csa, address_explorer_check, - add_pk): + add_pk, internal_type, skip_if_useless_way): + skip_if_useless_way(way) use_regtest() clear_miniscript() M, N = M_N + + ik = None # default static + if internal_type == "unspend(": + ik = f"unspend({os.urandom(32).hex()})/<20;21>/*" + elif internal_type == "xpub": + ik = ranged_unspendable_internal_key(os.urandom(32)) + ms_wo, _ = bitcoind_miniscript(M, N, "p2tr", funded=False, tapscript_threshold=csa, - add_own_pk=add_pk, way=way) + add_own_pk=add_pk, way=way, internal_key=ik) address_explorer_check(way, "bech32m", ms_wo, "minisc") @@ -982,10 +1056,19 @@ def test_bitcoind_tapscript_address(M_N, clear_miniscript, bitcoind_miniscript, @pytest.mark.parametrize("cc_first", [True, False]) @pytest.mark.parametrize("m_n", [(2,2), (3, 5), (32, 32)]) @pytest.mark.parametrize("way", ["qr", "sd"]) -@pytest.mark.parametrize("internal_key_spendable", [True, False, "77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76", "@"]) +@pytest.mark.parametrize("internal_key_spendable", [ + True, + False, + "77ec0c0fdb9733e6a3c753b1374c4a465cba80dff52fc196972640a26dd08b76", + "@", + "tpubD6NzVbkrYhZ4WhUnV3cPSoRWGf9AUdG2dvNpsXPiYzuTnxzAxemnbajrATDBWhaAVreZSzoGSe3YbbkY2K267tK3TrRmNiLH2pRBpo8yaWm/<2;3>/*", + "unspend(c72231504cf8c1bbefa55974db4e0cdac781049a9a81a87e7ff5beeb45b34d3d)/<0;1>/*" +]) def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, bitcoind, goto_home, cap_menu, pick_menu_item, cap_story, microsd_path, load_export, microsd_wipe, dev, way, - bitcoind_miniscript, clear_miniscript, get_cc_key, press_cancel, press_select): + bitcoind_miniscript, clear_miniscript, get_cc_key, press_cancel, press_select, + skip_if_useless_way, garbage_collector): + skip_if_useless_way(way) M, N = m_n clear_miniscript() microsd_wipe() @@ -993,10 +1076,13 @@ def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, r = None if internal_key_spendable is True: internal_key = get_cc_key("86h/0h/3h") - elif isinstance(internal_key_spendable, str) and len(internal_key_spendable) == 64: - r = internal_key_spendable elif internal_key_spendable == "@": r = "@" + elif isinstance(internal_key_spendable, str): + if len(internal_key_spendable) == 64: + r = internal_key_spendable + else: + internal_key = internal_key_spendable tapscript_wo, bitcoind_signers = bitcoind_miniscript( M, N, "p2tr", internal_key=internal_key, r=r, @@ -1011,8 +1097,12 @@ def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, for i in range(M - 1): signer = bitcoind_signers[i] psbt = signer.walletprocesspsbt(psbt, True, "DEFAULT", True)["psbt"] - with open(microsd_path(fname), "w") as f: + + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(psbt) + + garbage_collector.append(fpath) goto_home() # bug in goto_home ? press_cancel() @@ -1036,14 +1126,18 @@ def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, signed_fname = split_story[1] signed_txn_fname = split_story[-2] cc_tx_id = split_story[-1].split("\n")[-1] - with open(microsd_path(signed_txn_fname), "r") as f: + txn_fpath = microsd_path(signed_txn_fname) + with open(txn_fpath, "r") as f: signed_txn = f.read().strip() + garbage_collector.append(txn_fpath) else: signed_fname = split_story[-1] - with open(microsd_path(signed_fname), "r") as f: + fpath = microsd_path(signed_fname) + with open(fpath, "r") as f: signed_psbt = f.read().strip() + garbage_collector.append(fpath) if cc_first: for signer in bitcoind_signers: signed_psbt = signer.walletprocesspsbt(signed_psbt, True, "DEFAULT", True)["psbt"] @@ -1064,7 +1158,8 @@ def test_tapscript_multisig(cc_first, m_n, internal_key_spendable, use_regtest, def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bitcoind, internal_key_spendable, dev, microsd_path, get_cc_key, pick_menu_item, cap_story, goto_home, cap_menu, load_export, - import_miniscript, bitcoin_core_signer, import_duplicate, press_select): + import_miniscript, bitcoin_core_signer, import_duplicate, + press_select, garbage_collector): use_regtest() clear_miniscript() microsd_wipe() @@ -1095,13 +1190,16 @@ def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bi ) fname = "ts_pk.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc + "\n") + + garbage_collector.append(fpath) _, story = import_miniscript(fname) assert "Create new miniscript wallet?" in story assert fname.split(".")[0] in story assert "Taproot internal key" in story - assert "Taproot tree keys" in story + assert "Tapscript" in story assert "Press (1) to see extended public keys" in story assert "P2TR" in story @@ -1133,9 +1231,12 @@ def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bi dest_addr = ts.getnewaddress("", "bech32m") # selfspend psbt = ts.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"] fname = "ts_pk.psbt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(psbt) + garbage_collector.append(fpath) + goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1155,8 +1256,11 @@ def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bi press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() + + garbage_collector.append(fpath_psbt) # with open(microsd_path(fname_txn), "r") as f: # final_txn = f.read().strip() res = ts.finalizepsbt(final_psbt) @@ -1172,8 +1276,10 @@ def test_tapscript_pk(num_leafs, use_regtest, clear_miniscript, microsd_wipe, bi @pytest.mark.parametrize("desc", [ "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*),sortedmulti_a(2,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)},sortedmulti_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*)})#tpm3afjn", "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)}})", + "tr(tpubD6NzVbkrYhZ4XB7hZjurMYsPsgNY32QYGZ8YFVU7cy1VBRNoYpKAVuUfqfUFss6BooXRrCeYAdK9av2yFnqWXZaUMJuZdpE9Kuh6gubCVHu/<0;1>/*,{sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)}})", "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},sortedmulti_a(2,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)})", "tr(50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},or_d(pk([0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*),and_v(v:pkh([30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),older(500)))})", + "tr(unspend(b320077905d0954b01a8a328ea08c0ac3b4b066d1240f47a1b2c58651dcda4eb)/<0;1>/*,{{sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[b7fe820c/48'/1'/0'/3']tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/0/*),sortedmulti_a(2,[0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*,[30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*)},or_d(pk([0f056943/48'/1'/0'/3']tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/0/*),and_v(v:pkh([30afbe54/48'/1'/0'/3']tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/0/*),older(500)))})", ]) def test_tapscript_import_export(clear_miniscript, pick_menu_item, cap_story, import_miniscript, load_export, desc, microsd_path, @@ -1198,8 +1304,8 @@ def test_tapscript_import_export(clear_miniscript, pick_menu_item, cap_story, def test_duplicate_tapscript_leaves(use_regtest, clear_miniscript, microsd_wipe, bitcoind, dev, - goto_home, pick_menu_item, microsd_path, - cap_story, load_export, get_cc_key, import_miniscript, + goto_home, pick_menu_item, microsd_path, import_miniscript, + cap_story, load_export, get_cc_key, garbage_collector, bitcoin_core_signer, import_duplicate, press_select): # works in core - but some discussions are ongoing # https://github.com/bitcoin/bitcoin/issues/27104 @@ -1216,14 +1322,17 @@ def test_duplicate_tapscript_leaves(use_regtest, clear_miniscript, microsd_wipe, tmplt = tmplt % (cc_leaf, cc_leaf) desc = f"tr({core_key},{tmplt})" fname = "dup_leafs.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) + _, story = import_miniscript(fname) assert "Create new miniscript wallet?" in story assert fname.split(".")[0] in story assert "Taproot internal key" in story - assert "Taproot tree keys" in story + assert "Tapscript" in story assert "Press (1) to see extended public keys" in story assert "P2TR" in story @@ -1259,9 +1368,10 @@ def test_duplicate_tapscript_leaves(use_regtest, clear_miniscript, microsd_wipe, dest_addr = ts.getnewaddress("", "bech32m") # selfspend psbt = ts.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"] fname = "ts_pk.psbt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(psbt) - + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1281,8 +1391,10 @@ def test_duplicate_tapscript_leaves(use_regtest, clear_miniscript, microsd_wipe, press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() + garbage_collector.append(fpath_psbt) # with open(microsd_path(fname_txn), "r") as f: # final_txn = f.read().strip() res = ts.finalizepsbt(final_psbt) @@ -1298,7 +1410,7 @@ def test_duplicate_tapscript_leaves(use_regtest, clear_miniscript, microsd_wipe, def test_same_key_account_based_minisc(goto_home, pick_menu_item, cap_story, clear_miniscript, microsd_path, load_export, bitcoind, import_miniscript, use_regtest, import_duplicate, - press_select): + press_select, garbage_collector): clear_miniscript() use_regtest() @@ -1310,8 +1422,10 @@ def test_same_key_account_based_minisc(goto_home, pick_menu_item, cap_story, name = "mini-accounts" fname = f"{name}.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) _, story = import_miniscript(fname) assert "Create new miniscript wallet?" in story @@ -1350,9 +1464,10 @@ def test_same_key_account_based_minisc(goto_home, pick_menu_item, cap_story, dest_addr = wo.getnewaddress("", "bech32") # selfspend psbt = wo.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"] fname = "multi-acct.psbt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(psbt) - + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1372,8 +1487,10 @@ def test_same_key_account_based_minisc(goto_home, pick_menu_item, cap_story, press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() + garbage_collector.append(fpath_psbt) _psbt = BasicPSBT().parse(final_psbt.encode()) assert len(_psbt.inputs[0].part_sigs) == 2 @@ -1434,7 +1551,7 @@ CHANGE_BASED_DESCS = [ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, clear_miniscript, microsd_path, load_export, bitcoind, import_miniscript, address_explorer_check, use_regtest, - desc, press_select): + desc, press_select, garbage_collector): clear_miniscript() use_regtest() if desc.startswith("tr("): @@ -1444,8 +1561,10 @@ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, name = "mini-change" fname = f"{name}.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) _, story = import_miniscript(fname) assert "Create new miniscript wallet?" in story @@ -1483,9 +1602,10 @@ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, dest_addr = wo.getnewaddress("", af) # selfspend psbt = wo.walletcreatefundedpsbt([], [{dest_addr: 1.0}], 0, {"fee_rate": 2})["psbt"] fname = "msc-change-conso.psbt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(psbt) - + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1504,8 +1624,10 @@ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, assert "Updated PSBT is:" in story press_select() fname_psbt = story.split("\n\n")[1] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() + garbage_collector.append(fpath_psbt) res = wo.finalizepsbt(final_psbt) assert res["complete"] @@ -1526,9 +1648,10 @@ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, 0, {"fee_rate": 2} )["psbt"] fname = "msc-change-send.psbt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(psbt) - + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1547,9 +1670,10 @@ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, assert "Updated PSBT is:" in story press_select() fname_psbt = story.split("\n\n")[1] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() - + garbage_collector.append(fpath_psbt) res = wo.finalizepsbt(final_psbt) assert res["complete"] tx_hex = res["hex"] @@ -1564,7 +1688,7 @@ def test_same_key_change_based_minisc(goto_home, pick_menu_item, cap_story, def test_same_key_account_based_multisig(goto_home, pick_menu_item, cap_story, clear_miniscript, microsd_path, load_export, bitcoind, - import_miniscript): + import_miniscript, garbage_collector): clear_miniscript() desc = ("wsh(sortedmulti(2," "[0f056943/84'/1'/0']tpubDC7jGaaSE66Pn4dgtbAAstde4bCyhSUs4r3P8WhMVvPByvcRrzrwqSvpF9Ghx83Z1LfVugGRrSBko5UEKELCz9HoMv5qKmGq3fqnnbS5E9r/<0;1>/*," @@ -1572,8 +1696,10 @@ def test_same_key_account_based_multisig(goto_home, pick_menu_item, cap_story, "))") name = "multi-accounts" fname = f"{name}.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) _, story = import_miniscript(fname) assert "Failed to import" in story @@ -1587,20 +1713,23 @@ def test_same_key_account_based_multisig(goto_home, pick_menu_item, cap_story, "tr(%s,or_d(pk(@A),and_v(v:pkh(@A),older(5))))" % H, ]) def test_insane_miniscript(get_cc_key, pick_menu_item, cap_story, - microsd_path, desc, import_miniscript): + microsd_path, desc, import_miniscript, + garbage_collector): cc_key = get_cc_key("84h/0h/0h") desc = desc.replace("@A", cc_key) fname = "insane.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) _, story = import_miniscript(fname) assert "Failed to import" in story assert "Insane" in story def test_tapscript_depth(get_cc_key, pick_menu_item, cap_story, - microsd_path, import_miniscript): + microsd_path, import_miniscript, garbage_collector): leaf_num = 9 scripts = [] for i in range(leaf_num): @@ -1610,8 +1739,10 @@ def test_tapscript_depth(get_cc_key, pick_menu_item, cap_story, tree = TREE[leaf_num] % tuple(scripts) desc = f"tr({H},{tree})" fname = "9leafs.txt" - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) _, story = import_miniscript(fname) assert "Failed to import" in story assert "num_leafs > 8" in story @@ -1621,6 +1752,7 @@ def test_tapscript_depth(get_cc_key, pick_menu_item, cap_story, @pytest.mark.parametrize("same_acct", [True, False]) @pytest.mark.parametrize("recovery", [True, False]) @pytest.mark.parametrize("leaf2_mine", [True, False]) +@pytest.mark.parametrize("internal_type", ["unspend(", "xpub", "static"]) @pytest.mark.parametrize("minisc", [ "or_d(pk(@A),and_v(v:pkh(@B),locktime(N)))", @@ -1631,10 +1763,11 @@ def test_tapscript_depth(get_cc_key, pick_menu_item, cap_story, "or_d(pk(@A),and_v(v:multi_a(2,@B,@C),locktime(N)))", ]) def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, goto_home, - pick_menu_item, cap_menu, cap_story, microsd_path, + pick_menu_item, cap_menu, cap_story, microsd_path, internal_type, use_regtest, bitcoind, microsd_wipe, load_export, dev, address_explorer_check, get_cc_key, import_miniscript, - bitcoin_core_signer, same_acct, import_duplicate, press_select): + bitcoin_core_signer, same_acct, import_duplicate, press_select, + garbage_collector): # needs bitcoind 26.0 normal_cosign_core = False @@ -1683,10 +1816,16 @@ def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, if "@C" in minisc: minisc = minisc.replace("@C", core_keys[1]) + ik = H + if internal_type == "unspend(": + ik = f"unspend({os.urandom(32).hex()})/<2;3>/*" + elif internal_type == "xpub": + ik = ranged_unspendable_internal_key(os.urandom(32)) + if leaf2_mine: - desc = f"tr({H},{{{minisc},pk({cc_key1})}})" + desc = f"tr({ik},{{{minisc},pk({cc_key1})}})" else: - desc = f"tr({H},{{pk({core_keys[2]}),{minisc}}})" + desc = f"tr({ik},{{pk({core_keys[2]}),{minisc}}})" use_regtest() clear_miniscript() @@ -1696,6 +1835,8 @@ def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) + wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) @@ -1741,8 +1882,10 @@ def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, psbt = signers[1].walletprocesspsbt(psbt, True, "ALL")["psbt"] name = f"{name}.psbt" - with open(microsd_path(name), "w") as f: + fpath = microsd_path(name) + with open(fpath, "w") as f: f.write(psbt) + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1762,8 +1905,10 @@ def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] + fpath_psbt = microsd_path(fname_psbt) with open(microsd_path(fname_psbt), "r") as f: final_psbt = f.read().strip() + garbage_collector.append(fpath) # with open(microsd_path(fname_txn), "r") as f: # final_txn = f.read().strip() res = wo.finalizepsbt(final_psbt) @@ -1792,11 +1937,13 @@ def test_minitapscript(leaf2_mine, recovery, lt_type, minisc, clear_miniscript, "sh(wsh(or_d(pk([30afbe54/48h/1h/0h/3h]tpubDFLVv7cuiLjn3QcsCend5kn3yw5sx6Czazy7hZvdGX61v8pkU95k2Byz9M5jnabzeUg7qWtHYLeKQyCWWAHhUmQQMeZ4Dee2CfGR2TsZqrN/<0;1>/*),and_v(v:multi_a(2,[b7fe820c/48h/1h/0h/3h]tpubDFdQ1sNV53TbogAMPEd2egY5NXfbdKD1Mnr2iBrJrcwRHJbKC7tuuUMHT8SSHJ2VEKdCf5WYBMfevvWCnyJV53gYUT2wFyxEV8SuUTedBp7/<0;1>/*,[0f056943/48h/1h/0h/3h]tpubDF2rnouQaaYrY6CUWTapYkeFEs3h3qrzL4M52ZGoPeU9dkarJMtrw6VF1zJRGuGuAFxYS3kXtavfAwQPTQkU5dyNYpbgxcpftrR8H3U85Ez/<0;1>/*),older(500)))))", ]) def test_multi_mixin(desc, clear_miniscript, microsd_path, pick_menu_item, - cap_story, import_miniscript): + cap_story, import_miniscript, garbage_collector): clear_miniscript() fname = "imdesc.txt" + fpath = microsd_path(fname) with open(microsd_path(fname), "w") as f: f.write(desc) + garbage_collector.append(fpath) title, story = import_miniscript(fname) assert "Failed to import" in story @@ -1811,7 +1958,8 @@ def test_timelock_mixin(): @pytest.mark.parametrize("cc_first", [True, False]) def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, cap_story, cap_menu, load_export, microsd_path, use_regtest, clear_miniscript, cc_first, - address_explorer_check, import_miniscript, bitcoin_core_signer, press_select): + address_explorer_check, import_miniscript, bitcoin_core_signer, press_select, + garbage_collector): # check D wrapper u property for segwit v0 and v1 # https://github.com/bitcoin/bitcoin/pull/24906/files @@ -1838,10 +1986,10 @@ def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, ca name = "d_wrapper" fname = f"{name}.txt" - fpath = microsd_path(fname) with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) wo = bitcoind.create_wallet(wallet_name=name, disable_private_keys=True, blank=True, passphrase=None, avoid_reuse=False, descriptors=True) @@ -1898,8 +2046,10 @@ def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, ca to_sign_psbt = psbt name = f"{name}.psbt" - with open(microsd_path(name), "w") as f: + fpath = microsd_path(name) + with open(fpath, "w") as f: f.write(to_sign_psbt) + garbage_collector.append(fpath) goto_home() pick_menu_item("Ready To Sign") time.sleep(.1) @@ -1919,9 +2069,10 @@ def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, ca press_select() fname_psbt = story.split("\n\n")[1] # fname_txn = story.split("\n\n")[3] - with open(microsd_path(fname_psbt), "r") as f: + fpath_psbt = microsd_path(fname_psbt) + with open(fpath_psbt, "r") as f: final_psbt = f.read().strip() - + garbage_collector.append(fpath_psbt) assert final_psbt != to_sign_psbt # with open(microsd_path(fname_txn), "r") as f: # final_txn = f.read().strip() @@ -1952,7 +2103,7 @@ def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, ca def test_chain_switching(use_mainnet, use_regtest, settings_get, settings_set, clear_miniscript, goto_home, cap_menu, pick_menu_item, - import_miniscript, microsd_path, press_select): + import_miniscript, microsd_path, press_select, garbage_collector): clear_miniscript() use_regtest() @@ -1965,8 +2116,10 @@ def test_chain_switching(use_mainnet, use_regtest, settings_get, settings_set, fname_xtn0 = "XTN0.txt" for desc, fname in [(x, fname_xtn), (z, fname_btc), (y, fname_xtn0)]: - with open(microsd_path(fname), "w") as f: + fpath = microsd_path(fname) + with open(fpath, "w") as f: f.write(desc) + garbage_collector.append(fpath) # cannot import XPUBS when testnet/regtest enabled _, story = import_miniscript(fname_btc) diff --git a/testing/test_sign.py b/testing/test_sign.py index 531d7d3e..e7567894 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -2969,10 +2969,11 @@ def test_sorting_outputs_by_size(fake_txn, start_sign, cap_story, use_testnet, @pytest.mark.parametrize("chain", ["BTC", "XTN"]) @pytest.mark.parametrize("data", [ # (out_style, amount, is_change) + [("p2tr", 999999, 1)] + [("p2tr", 888888, 0)] * 12, [("p2pkh", 1000000, 0)] * 99, - [("p2wpkh", 1000000, 1),("p2wpkh-p2sh", 800000, 1)] * 27, + [("p2wpkh", 1000000, 1),("p2wpkh-p2sh", 800000, 1), ("p2tr", 600000, 1)] * 27, [("p2pkh", 1000000, 1)] * 11 + [("p2wpkh", 50000000, 0)] * 16, - [("p2pkh", 1000000, 1), ("p2wpkh", 50000000, 0), ("p2wpkh-p2sh", 800000, 1)] * 11, + [("p2pkh", 1000000, 1), ("p2wpkh", 50000000, 0), ("p2wpkh-p2sh", 800000, 1), ("p2tr", 100000, 0)] * 11, ]) def test_txout_explorer(psbtv2, chain, data, fake_txn, start_sign, settings_set, txout_explorer, cap_story): @@ -3100,8 +3101,7 @@ def test_taproot_keyspend(use_regtest, bitcoind_d_sim_watch, start_sign, end_sig title, story = cap_story() assert title == 'OK TO SEND?' assert "Consolidating" in story # self-spend - assert "1 ins - fee" in story # one input - assert "2 outs" in story # two outputs + assert " 1 input\n 2 outputs" in story addrs = story.split("\n\n")[3].split("\n")[-2:] assert len(addrs) == 2 for addr in addrs: @@ -3162,8 +3162,7 @@ def test_taproot_keyspend(use_regtest, bitcoind_d_sim_watch, start_sign, end_sig title, story = cap_story() assert title == 'OK TO SEND?' assert "Consolidating" in story # self-spend - assert "2 ins - fee" in story # five inputs - assert "3 outs" in story # one output + assert " 2 inputs\n 3 outputs" in story press_select() # confirm signing time.sleep(0.1) title, story = cap_story() @@ -3212,8 +3211,7 @@ def test_taproot_keyspend(use_regtest, bitcoind_d_sim_watch, start_sign, end_sig title, story = cap_story() assert title == 'OK TO SEND?' assert "Consolidating" in story # self-spend - assert "3 ins - fee" in story # five inputs - assert "1 outs" in story # one output + assert " 3 inputs\n 1 output" in story press_select() # confirm signing time.sleep(0.1) title, story = cap_story()