unspend( & ranged unspendable taproot internal keys

This commit is contained in:
scgbckbone 2024-06-25 15:00:53 +02:00
parent b1fe5e194d
commit ef2c5a7f1f
14 changed files with 581 additions and 267 deletions

View File

@ -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

44
releases/EdgeChangeLog.md Normal file
View File

@ -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)

54
releases/History-Edge.md Normal file
View File

@ -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":"<descriptor>"}` 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

View File

@ -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:

View File

@ -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()

View File

@ -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('<I', swab32(self.node.my_fp()))
origin = KeyOriginInfo(fp, [idx])
derivation = KeyDerivationInfo(self.derivation.indexes[1:])
return type(self)(new_node, origin, derivation, taproot=self.taproot)
@classmethod
@ -407,6 +399,8 @@ class Key:
def is_provably_unspendable(self):
if isinstance(self.node, bytes):
return True
if PROVABLY_UNSPENDABLE == self.node.pubkey():
return True
return False
@property
@ -445,6 +439,55 @@ class Key:
return cls.parse(s)
class Unspend(Key):
def __init__(self, node, origin=None, derivation=None, taproot=True, chain_type=None):
super().__init__(node, origin, derivation, taproot, chain_type)
assert self.taproot
def __eq__(self, other):
return self.node.chain_code() == other.node.chain_code() \
and self.node.pubkey() == other.node.pubkey() \
and self.derivation.indexes == other.derivation.indexes
@classmethod
def parse(cls, s):
assert s.read(8) == b"unspend("
chain_code, c = read_until(s, b")")
chain_code = a2b_hex(chain_code)
assert len(chain_code) == 32, "chain code length"
assert c
char = s.read(1)
if char != b"/":
raise ValueError("ranged unspend required")
der, char = read_until(s, b"<,)")
if char == b"<":
der += b"<"
branch, char = read_until(s, b">")
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(">")

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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}

View File

@ -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)

View File

@ -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()