unspend( & ranged unspendable taproot internal keys
This commit is contained in:
parent
b1fe5e194d
commit
ef2c5a7f1f
@ -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
44
releases/EdgeChangeLog.md
Normal 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
54
releases/History-Edge.md
Normal 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
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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(">")
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user