multi-day commit dump

This commit is contained in:
scgbckbone 2025-07-09 12:36:55 +02:00
parent 638e7acc55
commit 789b87c33c
34 changed files with 1777 additions and 2685 deletions

View File

@ -9,7 +9,7 @@ from ux import ux_show_story, the_ux, ux_enter_bip32_index
from ux import export_prompt_builder, import_export_prompt_decode
from menu import MenuSystem, MenuItem
from public_constants import AFC_BECH32, AFC_BECH32M, AF_P2WPKH, AF_P2TR, AF_CLASSIC
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
from uasyncio import sleep_ms
from uhashlib import sha256
from glob import settings

View File

@ -814,7 +814,7 @@ async def done_signing(psbt, tx_req, input_method=None, filename=None,
# for specific cases, key teleport is an option
offer_kt = False
if not is_complete and (psbt.active_multisig or psbt.active_miniscript) and version.has_qwerty:
if not is_complete and version.has_qwerty and psbt.active_miniscript:
offer_kt = 'use Key Teleport to send PSBT to other co-signers'
while True:
@ -1330,36 +1330,6 @@ def start_show_miniscript_address(msc, change, index):
# provide the value back to attached desktop
return UserAuthorizedAction.active_request.address
def start_show_p2sh_address(M, N, addr_format, xfp_paths, witdeem_script):
# Show P2SH address to user, also returns it.
# - first need to find appropriate multisig wallet associated
# - they must provide full redeem script, and we will re-verify it and check pubkeys inside it
from multisig import MultisigWallet
try:
assert addr_format in SUPPORTED_ADDR_FORMATS
assert addr_format & AFC_SCRIPT
except:
raise AssertionError('Unknown/unsupported addr format')
# Search for matching multisig wallet that we must already know about
xs = list(xfp_paths)
xs.sort()
ms = MultisigWallet.find_match(M, N, xs)
assert ms, 'Multisig wallet with those fingerprints not found'
assert ms.M == M
assert ms.N == N
UserAuthorizedAction.check_busy(ShowAddressBase)
UserAuthorizedAction.active_request = ShowP2SHAddress(ms, addr_format, xfp_paths, witdeem_script)
# kill any menu stack, and put our thing at the top
abort_and_goto(UserAuthorizedAction.active_request)
# provide the value back to attached desktop
return UserAuthorizedAction.active_request.address
def show_address(addr_format, subpath, restore_menu=False):
try:
@ -1394,7 +1364,7 @@ class MiniscriptDeleteRequest(UserAuthorizedAction):
self.wallet = msc
async def interact(self):
from miniscript import miniscript_delete
from wallet import miniscript_delete
await miniscript_delete(self.wallet)
self.done()
@ -1454,7 +1424,7 @@ class NewMiniscriptEnrollRequest(UserAuthorizedAction):
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None):
# Offer to import (enroll) a new multisig/miniscript wallet. Allow reject by user.
from glob import dis
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
UserAuthorizedAction.cleanup()
dis.fullscreen('Wait...')

View File

@ -183,7 +183,7 @@ class CCCFeature:
if not cls.is_enabled:
return False, False
ms = psbt.active_multisig
ms = psbt.active_miniscript
if not ms:
# single-sig CCC not supported
return False, False
@ -192,7 +192,7 @@ class CCCFeature:
# don't try to sign; maybe show warning?
xfp = cls.get_xfp()
if xfp not in ms.xfp_paths:
if xfp not in [i[0] for i in ms.to_descriptor().xfp_paths()]:
# does not involve us
return False, False
@ -253,7 +253,7 @@ class CCCConfigMenu(MenuSystem):
self.replace_items(tmp)
def construct(self):
from multisig import MultisigWallet, make_ms_wallet_menu
from wallet import MiniScriptWallet, make_miniscript_wallet_menu
my_xfp = CCCFeature.get_xfp()
items = [
@ -266,10 +266,13 @@ class CCCConfigMenu(MenuSystem):
# look for wallets that are defined related to CCC feature, shortcut to them
count = 0
for ms in MultisigWallet.get_all():
if my_xfp in ms.xfp_paths:
items.append(MenuItem('%d/%d: %s' % (ms.M, ms.N, ms.name),
menu=make_ms_wallet_menu, arg=ms.storage_idx))
for ms in MiniScriptWallet.get_all():
if not ms.m_n: # basic multisig check
continue
if my_xfp in [i[0] for i in ms.xfp_paths()]:
M, N = ms.m_n
items.append(MenuItem('%d/%d: %s' % (M, N, ms.name),
menu=make_miniscript_wallet_menu, arg=ms.storage_idx))
count += 1
items.append(MenuItem('↳ Build 2-of-N', f=self.build_2ofN, arg=count))
@ -331,7 +334,7 @@ class CCCConfigMenu(MenuSystem):
xfp = CCCFeature.get_xfp()
enc = CCCFeature.get_encoded_secret()
from miniscript import export_miniscript_xpubs
from wallet import export_miniscript_xpubs
await export_miniscript_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
async def build_2ofN(self, m, l, i):

View File

@ -519,7 +519,8 @@ def addr_fmt_label(addr_fmt):
AF_P2WPKH: "Segwit P2WPKH",
AF_P2TR: "Taproot P2TR",
AF_P2WSH: "Segwit P2WSH",
AF_P2WSH_P2SH: "P2SH-P2WSH"
AF_P2WSH_P2SH: "P2SH-P2WSH",
AF_P2SH: "Legacy P2SH",
}[addr_fmt]
def verify_recover_pubkey(sig, digest):

View File

@ -218,21 +218,6 @@ def decode_short_text(got):
# was something else.
pass
if ("\n" in got) and ('pub' in got):
# legacy multisig import/export format
# [0-9a-fA-F]{8}\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]{107}
# above is more precise BUT counted repetitions not supported in mpy
cc_ms_pat = r"[0-9a-fA-F]+\s*:\s*[xtyYzZuUvV]pub[1-9A-HJ-NP-Za-km-z]+"
rgx = ure.compile(cc_ms_pat)
# go line by line and match above, once 2 matches observed - considered multisig
# important to not use ure.search for big strings (can run out of stack)
c = 0 # match count
for l in got.split("\n"):
if rgx.search(l):
c += 1
if c > 1:
return 'multi', (got,)
from descriptor import Descriptor
if Descriptor.is_descriptor(got):
return 'minisc', (got,)

View File

@ -4,7 +4,7 @@
#
import ngu, chains, ustruct, stash
from io import BytesIO
from public_constants import AF_P2SH, AF_P2WSH_P2SH, AF_P2WSH, AF_CLASSIC, AF_P2TR
from public_constants import MAX_PATH_DEPTH
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from utils import keypath_to_str, str_to_keypath, swab32, xfp2str
@ -80,23 +80,6 @@ def parse_desc_str(string):
return res
def multisig_descriptor_template(xpub, path, xfp, addr_fmt):
key_exp = "[%s%s]%s/0/*" % (xfp.lower(), path.replace("m", ''), xpub)
if addr_fmt == AF_P2WSH_P2SH:
descriptor_template = "sh(wsh(sortedmulti(M,%s,...)))"
elif addr_fmt == AF_P2WSH:
descriptor_template = "wsh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2SH:
descriptor_template = "sh(sortedmulti(M,%s,...))"
elif addr_fmt == AF_P2TR:
# provably unspendable BIP-0341
descriptor_template = "tr(" + b2a_hex(PROVABLY_UNSPENDABLE[1:]).decode() + ",sortedmulti_a(M,%s,...))"
else:
return None
descriptor_template = descriptor_template % key_exp
return descriptor_template
def read_until(s, chars=b",)(#"):
res = b""
while True:
@ -143,6 +126,7 @@ class KeyOriginInfo:
arr[0] = "m"
path = "/".join(arr)
derivation = str_to_keypath(xfp, path)[1:] # ignoring xfp here, already stored
assert len(derivation) <= MAX_PATH_DEPTH, "origin too deep"
return cls(xfp, derivation)
def __str__(self):
@ -212,8 +196,8 @@ class KeyDerivationInfo:
obj = cls()
else:
if multi_i is not None:
assert len(idxs[multi_i]) == 2, "wrong multipath"
assert multi_i is not None, "need multipath"
assert len(idxs[multi_i]) == 2, "wrong multipath"
obj = cls(tuple(idxs))
obj.multi_path_index = multi_i
@ -312,17 +296,27 @@ class Key:
return node, chain_type
def validate(self, my_xfp):
def validate(self, my_xfp, disable_checks=False):
assert self.chain_type == chains.current_key_chain().ctype, "wrong chain"
depth = self.node.depth()
# xfp is always available, even if key was serialized without origin info
# upon parse root origin info is generated from key itself
xfp = self.origin.cc_fp
if depth == 1:
target = swab32(self.node.parent_fp())
assert xfp == target, 'xfp depth=1 wrong'
if not disable_checks:
depth = self.node.depth()
# TODO we now allow blinded keys that have depth X bud derivation len is 0
# print("depth", depth)
# print("origin der", self.origin.derivation)
# assert len(self.origin.derivation) == depth, "deriv len != xpub depth (xfp=%s)" % xfp2str(xfp)
if depth == 0:
assert swab32(self.node.my_fp()) == xfp, "master xfp mismatch"
elif depth == 1:
target = swab32(self.node.parent_fp())
assert xfp == target, 'xfp depth=1 wrong'
if xfp == my_xfp:
is_mine = (xfp == my_xfp)
if is_mine and not disable_checks:
# it's supposed to be my key, so I should be able to generate pubkey
# - might indicate collision on xfp value between co-signers,
# and that's not supported
@ -331,8 +325,8 @@ class Key:
chk_node = sv.derive_path(deriv)
assert self.node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
return 1
return 0
return is_mine
def derive(self, idx=None, change=False):
@ -371,16 +365,21 @@ class Key:
@classmethod
def from_cc_data(cls, xfp, deriv, xpub):
koi = KeyOriginInfo.from_string("%s/%s" % (xfp2str(xfp), deriv.replace("m/", "")))
node = ngu.hdnode.HDNode()
node.deserialize(xpub)
return cls(node, koi, KeyDerivationInfo())
xfp_str = xfp if isinstance(xfp, str) else xfp2str(xfp)
koi = KeyOriginInfo.from_string("%s/%s" % (xfp_str, deriv.replace("m/", "")))
node, chain_type = cls.parse_key(xpub.encode())
def to_cc_data(self):
ch = chains.current_chain()
return (self.origin.cc_fp,
self.origin.str_derivation(),
ch.serialize_public(self.node, AF_CLASSIC))
return cls(node, koi, KeyDerivationInfo(), chain_type=chain_type)
@classmethod
def from_cc_json(cls, vals, af_str):
key_exp = af_str + "_key_exp"
if key_exp in vals:
# new firmware, prefer key expression
return cls.from_string(vals[key_exp])
ek = chains.slip32_deserialize(vals[af_str])
return cls.from_cc_data(vals["xfp"], vals["%s_deriv" % af_str], ek)
@property
def is_provably_unspendable(self):

View File

@ -186,7 +186,6 @@ class Descriptor:
def xfp_paths(self, skip_unspend_ik=False):
res = []
for k in self.keys:
if self.is_taproot and k.is_provably_unspendable and skip_unspend_ik:
continue

View File

@ -390,7 +390,6 @@ def generate_unchained_export(account_num=0):
def generate_generic_export(account_num=0):
# Generate data that other programers will use to import Coldcard (single-signer)
from descriptor import Descriptor, Key
from desc_utils import multisig_descriptor_template
chain = chains.current_chain()
master_xfp = settings.get("xfp")
@ -422,7 +421,9 @@ def generate_generic_export(account_num=0):
xp = chain.serialize_public(node, AF_CLASSIC)
zp = chain.serialize_public(node, fmt) if fmt not in (AF_CLASSIC, AF_P2TR) else None
if is_ms:
desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
# TODO
# desc = multisig_descriptor_template(xp, dd, master_xfp_str, fmt)
pass
else:
key = Key.from_cc_data(master_xfp, dd, xp)
desc_obj = Descriptor(key=key, addr_fmt=fmt)

View File

@ -9,8 +9,7 @@ from glob import settings
from actions import *
from choosers import *
from mk4 import dev_enable_repl
from multisig import make_multisig_menu, import_multisig_nfc
from miniscript import make_miniscript_menu
from wallet import make_miniscript_menu, import_miniscript_nfc
from seed import make_ephemeral_seed_menu, make_seed_vault_menu, start_b39_pw
from address_explorer import address_explore
from drv_entro import drv_entro_start, password_entry
@ -143,8 +142,6 @@ SettingsMenu = [
# xxxxxxxxxxxxxxxx
MenuItem('Login Settings', menu=LoginPrefsMenu),
MenuItem('Hardware On/Off', menu=HWTogglesMenu),
NonDefaultMenuItem('Multisig Wallets', 'multisig',
menu=make_multisig_menu, predicate=has_secrets),
NonDefaultMenuItem('Miniscript', 'miniscript',
menu=make_miniscript_menu, predicate=has_secrets, shortcut="m"),
NonDefaultMenuItem('NFC Push Tx', 'ptxurl', menu=pushtx_setup_menu),
@ -357,7 +354,7 @@ NFCToolsMenu = [
MenuItem('Verify Sig File', f=nfc_sign_verify),
MenuItem('Verify Address', f=nfc_address_verify),
MenuItem('File Share', f=nfc_share_file),
MenuItem('Import Multisig', f=import_multisig_nfc),
MenuItem('Import Miniscript', f=import_miniscript_nfc),
MenuItem('Push Transaction', f=nfc_pushtx_file, predicate=lambda: settings.get("ptxurl", False)),
]

View File

@ -11,7 +11,7 @@ from pincodes import AE_LONG_SECRET_LEN
from stash import blank_object
from users import Users, MAX_NUMBER_USERS, calc_local_pincode
from public_constants import MAX_USERNAME_LEN
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
from ubinascii import hexlify as b2a_hex
from uhashlib import sha256
from ucollections import OrderedDict

View File

@ -2,815 +2,12 @@
#
# Copyright (c) 2020 Stepan Snigirev MIT License embit/miniscript.py
#
import ngu, ujson, uio, chains, ure, version, stash
import ngu
from binascii import unhexlify as a2b_hex
from binascii import hexlify as b2a_hex
from serializations import ser_compact_size, ser_string
from desc_utils import Key, read_until, bip388_wallet_policy_to_descriptor, append_checksum, bip388_validate_policy
from public_constants import MAX_TR_SIGNERS, AF_P2TR
from wallet import BaseStorageWallet, MAX_BIP32_IDX
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, to_ascii_printable, swab32, show_single_address
from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC, KEY_ENTER
from glob import settings
# Arbitrary value, not 0 or 1, used to derive a pubkey from preshared xpub in Key Teleport
KT_RXPUBKEY_DERIV = const(20250317)
# PSBT Xpub trust policies
TRUST_VERIFY = const(0)
TRUST_OFFER = const(1)
TRUST_PSBT = const(2)
class MiniScriptWallet(BaseStorageWallet):
key_name = "miniscript"
disable_checks = False
def __init__(self, name, desc_tmplt=None, keys_info=None, desc=None,
af=None, ik_u=None):
assert (desc_tmplt and keys_info) or desc
super().__init__()
self.name = name
self.desc_tmplt = desc_tmplt
self.keys_info = keys_info
self.desc = desc
self.addr_fmt = af
self.ik_u = ik_u
@classmethod
def get_trust_policy(cls):
which = settings.get('pms', None)
if which is None:
which = TRUST_VERIFY if cls.exists() else TRUST_OFFER
return which
@property
def chain(self):
return chains.current_chain()
def serialize(self):
return self.name, self.desc_tmplt, self.keys_info, self.addr_fmt, self.ik_u
@classmethod
def deserialize(cls, c, idx=-1):
# after deserialization - we lack loaded descriptor object
# we do not need it for everything
name, desc_tmplt, keys_info, af, ik_u = c
rv = cls(name, desc_tmplt, keys_info, af=af, ik_u=ik_u)
rv.storage_idx = idx
return rv
def to_descriptor(self, validate=False):
if self.desc is None:
# actual descriptor is not loaded, but was asked for
# fill policy - aka storage format - to actual descriptor
from descriptor import Descriptor
import glob
if self.name in glob.DESC_CACHE:
# loaded descriptor from cache
print("to_descriptor CACHE")
self.desc = glob.DESC_CACHE[self.name]
else:
desc_str = bip388_wallet_policy_to_descriptor(self.desc_tmplt, self.keys_info)
print("loading... filled policy:\n", desc_str)
# no need to validate already saved descriptor - was validated upon enroll
self.desc = Descriptor.from_string(desc_str, validate=validate)
# cache len always 1
glob.DESC_CACHE = {}
glob.DESC_CACHE[self.name] = self.desc
return self.desc
@classmethod
def find_match(cls, xfp_paths, addr_fmt=None):
for rv in cls.iter_wallets():
if addr_fmt is not None:
if rv.addr_fmt != addr_fmt:
continue
if rv.matching_subpaths(xfp_paths):
return rv
return None
def matching_subpaths(self, xfp_paths):
my_xfp_paths = self.to_descriptor().xfp_paths()
if len(xfp_paths) != len(my_xfp_paths):
return False
for x in my_xfp_paths:
prefix_len = len(x)
for y in xfp_paths:
if x == y[:prefix_len]:
break
else:
return False
return True
def subderivation_indexes(self, xfp_paths):
# we already know that they do match
my_xfp_paths = self.to_descriptor().xfp_paths()
res = set()
for x in my_xfp_paths:
prefix_len = len(x)
for y in xfp_paths:
if x == y[:prefix_len]:
to_derive = tuple(y[prefix_len:])
res.add(to_derive)
assert res
if len(res) == 1:
branch, idx = list(res)[0]
else:
branch = [i[0] for i in res]
indexes = set([i[1] for i in res])
assert len(indexes) == 1
idx = list(indexes)[0]
return branch, idx
def get_my_deriv(self, my_xfp):
# lowest public key from lexicographically sorted list is at index 0
mine = self.xpubs_from_xfp(my_xfp)
return mine[0].origin.str_derivation()
def derive_desc(self, xfp_paths):
branch, idx = self.subderivation_indexes(xfp_paths)
derived_desc = self.desc.derive(branch).derive(idx)
return derived_desc
def validate_script_pubkey(self, script_pubkey, xfp_paths, merkle_root=None):
derived_desc = self.derive_desc(xfp_paths)
derived_spk = derived_desc.script_pubkey()
assert derived_spk == script_pubkey, "spk mismatch"
if merkle_root:
assert derived_desc.tapscript.merkle_root == merkle_root, "psbt merkle root"
return derived_desc
async def _detail(self, new_wallet=False, is_duplicate=False):
s = chains.addr_fmt_label(self.addr_fmt) + "\n\n"
s += self.desc_tmplt
story = s + "\n\nPress (1) to see extended public keys"
if new_wallet and not is_duplicate:
story += ", OK to approve, X to cancel."
return story
async def show_detail(self, new_wallet=False, duplicates=None):
title = self.name
story = ""
if duplicates:
title = None
story += "This wallet is a duplicate of already saved wallet %s\n\n" % duplicates[0].name
elif new_wallet:
title = None
story += "Create new miniscript wallet?\n\nWallet Name:\n %s\n\n" % self.name
story += (chains.addr_fmt_label(self.addr_fmt) + "\n\n" + self.desc_tmplt)
story += "\n\nPress (1) to see extended public keys"
if new_wallet and not duplicates:
story += ", OK to approve, X to cancel."
while True:
ch = await ux_show_story(story, title=title, escape="1")
if ch == "1":
await self.show_keys()
elif ch != "y":
return None
else:
return True
async def show_keys(self):
msg = ""
for idx, k_str in enumerate(self.keys_info):
if idx:
msg += '\n---===---\n\n'
elif self.addr_fmt == AF_P2TR:
# index 0, taproot internal key
msg += "Taproot internal key:\n\n"
if self.ik_u:
msg += "(provably unspendable)\n\n"
msg += '@%s:\n %s\n\n' % (idx, k_str)
await ux_show_story(msg)
@classmethod
def from_bip388_wallet_policy(cls, name, desc_template, keys_info):
bip388_validate_policy(desc_template, keys_info)
msc = cls(name, desc_template, keys_info)
msc.to_descriptor(validate=True)
return msc
@classmethod
def from_file(cls, config, name=None, bip388=False):
from descriptor import Descriptor
if bip388:
# config is JSON wallet policy
wal = cls.from_bip388_wallet_policy(config["name"], config["desc_template"],
config["keys_info"])
else:
if name is None:
desc_obj, cs = Descriptor.from_string(config.strip(), checksum=True)
name = cs
else:
name = to_ascii_printable(name)
desc_obj = Descriptor.from_string(config.strip())
wal = cls(name, desc=desc_obj)
# BIP388 wasn't generated yet - generating from descriptor upon import/enroll
wal.desc_tmplt, wal.keys_info = desc_obj.bip388_wallet_policy()
bip388_validate_policy(wal.desc_tmplt, wal.keys_info)
wal.ik_u = wal.desc.key and wal.desc.key.is_provably_unspendable
wal.addr_fmt = wal.desc.addr_fmt
return wal
def find_duplicates(self):
matches = []
name_unique = True
for rv in self.iter_wallets():
if self.name == rv.name:
name_unique = False
if self.desc_tmplt != rv.desc_tmplt:
continue
if self.keys_info != rv.keys_info:
continue
matches.append(rv)
return matches, name_unique
async def confirm_import(self):
nope, yes = (KEY_CANCEL, KEY_ENTER) if version.has_qwerty else ("x", "y")
dups, name_unique = self.find_duplicates()
if not name_unique:
await ux_show_story(title="FAILED", msg=("Miniscript wallet with name '%s'"
" already exists. All wallets MUST"
" have unique names.") % self.name)
return nope
to_save = await self.show_detail(new_wallet=True, duplicates=dups)
ch = yes if to_save else nope
if to_save and not dups:
assert self.storage_idx == -1
self.commit()
import glob
# new wallet was imported - cache descriptor
glob.DESC_CACHE = {}
assert self.desc
glob.DESC_CACHE[self.name] = self.desc
await ux_dramatic_pause("Saved.", 2)
return ch
def yield_addresses(self, start_idx, count, change=False, scripts=False, change_idx=0):
ch = chains.current_chain()
dd = self.to_descriptor().derive(None, change=change)
idx = start_idx
while count:
if idx > MAX_BIP32_IDX:
break
# make the redeem script, convert into address
d = dd.derive(idx)
scr = d.miniscript.compile() if d.miniscript else None
addr = ch.render_address(d.script_pubkey(compiled_scr=scr))
ders = script = None
if scripts:
ders = ["[%s]" % str(k.origin) for k in d.keys]
if d.tapscript:
script = d.tapscript.script_tree()
else:
script = b2a_hex(ser_string(scr)).decode()
yield idx, addr, ders, script
idx += 1
count -= 1
def make_addresses_msg(self, msg, start, n, change=0):
from glob import dis
addrs = []
for idx, addr, *_ in self.yield_addresses(start, n, change=bool(change), scripts=False):
msg += '.../%d =>\n' % idx # just idx, if derivations or scripts needed - export csv
addrs.append(addr)
msg += show_single_address(addr) + '\n\n'
dis.progress_sofar(idx - start + 1, n)
return msg, addrs
def generate_address_csv(self, start, n, change):
yield '"' + '","'.join(
['Index', 'Payment Address']
) + '"\n'
for idx, addr, ders, script in self.yield_addresses(start, n, change=bool(change)):
ln = '%d,"%s"' % (idx, addr)
if ders:
ln += ',"%s","' % script
ln += '","'.join(ders)
ln += '"'
ln += '\n'
yield ln
def to_string(self, checksum=True):
# policy filling - not posible to specify internal/external always multipath export
# only supported from bitcoin-core 29.0
if self.desc_tmplt and self.keys_info:
desc = bip388_wallet_policy_to_descriptor(self.desc_tmplt, self.keys_info)
if checksum:
desc = append_checksum(desc)
return desc
return self.desc.to_string()
def bitcoin_core_serialize(self):
return [{
"desc": self.to_string(), # policy fill
"active": True,
"timestamp": "now",
"range": [0, 100],
}]
async def export_wallet_file(self, extra_msg=None, core=False, bip388=False):
# do not load descriptor - just fill policy
# only with multipath format <0;1>
from glob import NFC, dis
from ux import import_export_prompt
dis.fullscreen('Wait...')
if core:
name = "Bitcoin Core miniscript"
fname_pattern = 'bitcoin-core-%s.txt' % self.name
msg = "importdescriptors cmd"
core_obj = self.bitcoin_core_serialize()
core_str = ujson.dumps(core_obj)
res = "importdescriptors '%s'\n" % core_str
elif bip388:
# policy as JSON
name = "BIP-388 Wallet Policy"
fname_pattern = 'b388-%s.json' % self.name
res = ujson.dumps({"name": self.name,
"desc_template": self.desc_tmplt.replace("/<0;1>/*", "/**"),
"keys_info": self.keys_info})
else:
name = "Miniscript"
fname_pattern = 'minsc-%s.txt' % self.name
msg = self.name
res = self.to_string()
ch = await import_export_prompt("%s file" % name)
if isinstance(ch, str):
if ch in "3"+KEY_NFC:
await NFC.share_text(res)
elif ch == KEY_QR:
try:
from ux import show_qr_code
await show_qr_code(res, msg=msg)
except:
if version.has_qwerty:
from ux_q1 import show_bbqr_codes
await show_bbqr_codes('U', res, msg)
return
try:
with CardSlot(**ch) as card:
fname, nice = card.pick_filename(fname_pattern)
# do actual write
with open(fname, 'w+') as fp:
fp.write(res)
# fp.seek(0)
# contents = fp.read()
# TODO re-enable once we know how to proceed with regards to with which key to sign
# TODO need function to get my xpub from just policy
# from auth import write_sig_file
# h = ngu.hash.sha256s(contents.encode())
# sig_nice = write_sig_file([(h, fname)])
msg = '%s file written:\n\n%s' % (name, nice)
# msg += '\n\nColdcard multisig signature file written:\n\n%s' % sig_nice
if extra_msg:
msg += extra_msg
await ux_show_story(msg)
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
return
def xpubs_from_xfp(self, xfp):
# return list of XPUB's which match xfp
res = []
desc = self.to_descriptor()
for k in desc.keys:
if k.origin and k.origin.cc_fp == xfp:
res.append(k)
elif swab32(k.node.my_fp()) == xfp:
res.append(k)
assert res, "missing xfp %s" % xfp2str(xfp)
# returned is list of keys with corresponding master xfp
# key in list are lexicographically sorted based on their public keys
# lowest public key first
return sorted(res, key=lambda o: o.serialize())
def kt_make_rxkey(self, xfp):
# Derive the receiver's pubkey from preshared xpub and a special derivation
# - also provide the keypair we're using from our side of connection
# - returns 4 byte nonce which is sent un-encrypted, his_pubkey and my_keypair
ri = ngu.random.uniform(1<<28)
# sorted lexicographically, always use the lowest pubkey from the list at index 0
keys = self.xpubs_from_xfp(xfp)
k = keys[0]
k = k.derive(KT_RXPUBKEY_DERIV).derive(ri)
pubkey = k.node.pubkey()
kp = self.kt_my_keypair(ri)
return ri.to_bytes(4, 'big'), pubkey, kp
def kt_my_keypair(self, ri):
# Calc my keypair for sending PSBT files.
#
# sorted lexicographically, always use the lowest pubkey from the list at index 0
keys = self.xpubs_from_xfp(settings.get('xfp'))
subpath = "/%d/%d" % (KT_RXPUBKEY_DERIV, ri)
path = keys[0].origin.str_derivation() + subpath
with stash.SensitiveValues() as sv:
node = sv.derive_path(path)
kp = ngu.secp256k1.keypair(node.privkey())
return kp
@classmethod
def kt_search_rxkey(cls, payload):
# Construct the keypair for to be decryption
# - has to try pubkey each all the unique XFP for all co-signers in all wallets
# - checks checksum of ECDH unwrapped data to see if it's the right one
# - returns session key, decrypted first layer, and XFP of sender
from teleport import decode_step1
# this nonce is part of the derivation path so each txn gets new keys
ri = int.from_bytes(payload[0:4], 'big')
my_xfp = settings.get('xfp')
for msc in cls.iter_wallets():
kp = msc.kt_my_keypair(ri)
for k in msc.to_descriptor().keys:
if k.origin.cc_fp == my_xfp:
continue
kk = k.derive(KT_RXPUBKEY_DERIV).derive(ri)
his_pubkey = kk.node.pubkey()
# if implied session key decodes the checksum, it is right
ses_key, body = decode_step1(kp, his_pubkey, payload[4:])
if ses_key:
return ses_key, body, kk.origin.cc_fp
return None, None, None
async def no_miniscript_yet(*a):
await ux_show_story("You don't have any miniscript wallets yet.")
async def miniscript_delete(msc):
if not await ux_confirm("Delete miniscript wallet '%s'?\n\nFunds may be impacted." % msc.name):
await ux_dramatic_pause('Aborted.', 3)
return
msc.delete()
await ux_dramatic_pause('Deleted.', 3)
async def miniscript_wallet_delete(menu, label, item):
msc = item.arg
await miniscript_delete(msc)
from ux import the_ux
# pop stack
the_ux.pop()
m = the_ux.top_of_stack()
m.update_contents()
async def miniscript_wallet_detail(menu, label, item):
# show details of single multisig wallet
msc = item.arg
return await msc.show_detail()
async def import_miniscript(*a):
# pick text file from SD card, import as multisig setup file
from actions import file_picker
from ux import import_export_prompt
ch = await import_export_prompt("miniscript wallet file", is_import=True)
if isinstance(ch, str):
if ch == KEY_QR:
await import_miniscript_qr()
elif ch == KEY_NFC:
await import_miniscript_nfc()
return
def possible(filename):
with open(filename, 'rt') as fd:
for ln in fd:
if "sh(" in ln or "wsh(" in ln or "tr(" in ln:
# descriptor import
return True
fn = await file_picker(suffix=['.txt', '.json'], min_size=100,
taster=possible, **ch)
if not fn: return
try:
with CardSlot(**ch) as card:
with open(fn, 'rt') as fp:
data = fp.read()
except CardMissingError:
await needs_microsd()
return
from auth import maybe_enroll_xpub
try:
possible_name = (fn.split('/')[-1].split('.'))[0] if fn else None
maybe_enroll_xpub(config=data, name=possible_name)
except BaseException as e:
await ux_show_story('Failed to import miniscript.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_miniscript_nfc(*a):
from glob import NFC
try:
return await NFC.import_miniscript_nfc()
except Exception as e:
await ux_show_story('Failed to import miniscript.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_miniscript_qr(*a):
from auth import maybe_enroll_xpub
from ux_q1 import QRScannerInteraction
data = await QRScannerInteraction().scan_text('Scan Miniscript from a QR code')
if not data:
# press pressed CANCEL
return
try:
maybe_enroll_xpub(config=data)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def miniscript_wallet_export(menu, label, item):
# create a text file with the details; ready for import to next Coldcard
msc = item.arg[0]
kwargs = item.arg[1]
await msc.export_wallet_file(**kwargs)
async def make_miniscript_wallet_descriptor_menu(menu, label, item):
# descriptor menu
msc = item.arg
if not msc:
return
rv = [
MenuItem('Export', f=miniscript_wallet_export, arg=(msc, {"core": False})),
MenuItem('Bitcoin Core', f=miniscript_wallet_export, arg=(msc, {"core": True})),
MenuItem('BIP-388 Policy', f=miniscript_wallet_export, arg=(msc, {"bip388":True})),
]
return rv
async def make_miniscript_wallet_menu(menu, label, item):
# details, actions on single multisig wallet
msc = MiniScriptWallet.get_by_idx(item.arg)
if not msc: return
rv = [
MenuItem('"%s"' % msc.name, f=miniscript_wallet_detail, arg=msc),
MenuItem('View Details', f=miniscript_wallet_detail, arg=msc),
MenuItem('Delete', f=miniscript_wallet_delete, arg=msc),
MenuItem('Descriptors', menu=make_miniscript_wallet_descriptor_menu, arg=msc),
]
return rv
class MiniscriptMenu(MenuSystem):
@classmethod
def construct(cls):
import version
from menu import ShortcutItem
if not MiniScriptWallet.exists():
rv = [MenuItem(MiniScriptWallet.none_setup_yet(), f=no_miniscript_yet)]
else:
rv = []
for msc in MiniScriptWallet.get_all():
rv.append(MenuItem('%s' % msc.name,
menu=make_miniscript_wallet_menu,
arg=msc.storage_idx))
from glob import NFC
rv.append(MenuItem('Import', f=import_miniscript))
rv.append(MenuItem('Export XPUB', f=export_miniscript_xpubs))
rv.append(MenuItem('BSMS (BIP-129)', menu=make_ms_wallet_bsms_menu))
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
rv.append(MenuItem('Skip Checks?', f=disable_checks_menu))
rv.append(ShortcutItem(KEY_NFC, predicate=lambda: NFC is not None,
f=import_miniscript_nfc))
rv.append(ShortcutItem(KEY_QR, predicate=lambda: version.has_qwerty,
f=import_miniscript_qr))
return rv
def update_contents(self):
# Reconstruct the list of wallets on this dynamic menu, because
# we added or changed them and are showing that same menu again.
tmp = self.construct()
self.replace_items(tmp)
async def make_miniscript_menu(*a):
# list of all multisig wallets, and high-level settings/actions
from pincodes import pa
if pa.is_secret_blank():
await ux_show_story("You must have wallet seed before creating miniscript wallets.")
return
rv = MiniscriptMenu.construct()
return MiniscriptMenu(rv)
def disable_checks_chooser():
ch = ['Normal', 'Skip Checks']
def xset(idx, text):
MiniScriptWallet.disable_checks = bool(idx)
return int(MiniScriptWallet.disable_checks), ch, xset
async def disable_checks_menu(*a):
if not MiniScriptWallet.disable_checks:
ch = await ux_show_story('''\
With many different wallet vendors and implementors involved, it can \
be hard to create a PSBT consistent with the many keys involved. \
With this setting, you can \
disable the more stringent verification checks your Coldcard normally provides.
USE AT YOUR OWN RISK. These checks exist for good reason! Signed txn may \
not be accepted by network.
This settings lasts only until power down.
Press (4) to confirm entering this DANGEROUS mode.
''', escape='4')
if ch != '4': return
start_chooser(disable_checks_chooser)
def psbt_xpubs_policy_chooser():
# Chooser for trust policy
ch = ['Verify Only', 'Offer Import', 'Trust PSBT']
def xset(idx, text):
settings.set('pms', idx)
return MiniScriptWallet.get_trust_policy(), ch, xset
async def trust_psbt_menu(*a):
# show a story then go into chooser
ch = await ux_show_story('''\
This setting controls what the Coldcard does \
with the co-signer public keys (XPUB) that may \
be provided inside a PSBT file. Three choices:
- Verify Only. Do not import the xpubs found, but do \
verify the correct wallet already exists on the Coldcard.
- Offer Import. If it's a new multisig wallet, offer to import \
the details and store them as a new wallet in the Coldcard.
- Trust PSBT. Use the wallet data in the PSBT as a temporary,
multisig wallet, and do not import it. This permits some \
deniability and additional privacy.
When the XPUB data is not provided in the PSBT, regardless of the above, \
we require the appropriate multisig wallet to already exist \
on the Coldcard. Default is to 'Offer' unless a multisig wallet already \
exists, otherwise 'Verify'.''')
if ch == 'x': return
start_chooser(psbt_xpubs_policy_chooser)
async def ms_wallet_electrum_export(menu, label, item):
# create a JSON file that Electrum can use. Challenges:
# - file contains derivation paths for each co-signer to use
# - electrum is using BIP-43 with purpose=48 (purpose48_derivation) to make paths like:
# m/48h/1h/0h/2h
# - above is now called BIP-48
# - other signers might not be coldcards (we don't know)
# solution:
# - when building air-gap, pick address type at that point, and matching path to suit
# - could check path prefix and addr_fmt make sense together, but meh.
ms = item.arg
from actions import electrum_export_story
derivs, dsum = ms.get_deriv_paths()
msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % (
dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) )
if await ux_show_story(electrum_export_story(msg)) != 'y':
return
await ms.export_electrum()
async def export_miniscript_xpubs(*a, xfp=None, alt_secret=None, skip_prompt=False):
# WAS: Create a single text file with lots of docs, and all possible useful xpub values.
# THEN: Just create the one-liner xpub export value they need/want to support BIP-45
# NOW: Export JSON with one xpub per useful address type and semi-standard derivation path
#
# - consumer for this file is supposed to be ourselves, when we build on-device multisig.
# - however some 3rd parties are making use of it as well.
# - used for CCC feature now as well, but result looks just like normal export
#
xfp = xfp2str(xfp or settings.get('xfp', 0))
chain = chains.current_chain()
fname_pattern = 'ccxp-%s.json' % xfp
label = "Multisig XPUB"
if not skip_prompt:
msg = '''\
This feature creates a small file containing \
the extended public keys (XPUB) you would need to join \
a multisig wallet.
Public keys for BIP-48 conformant paths are used:
P2SH-P2WSH:
m/48h/{coin}h/{{acct}}h/1h
P2WSH:
m/48h/{coin}h/{{acct}}h/2h
P2TR:
m/48h/{coin}h/{{acct}}h/3h
{ok} to continue. {x} to abort.'''.format(coin=chain.b44_cointype, ok=OK, x=X)
ch = await ux_show_story(msg)
if ch != "y":
return
acct = await ux_enter_bip32_index('Account Number:') or 0
def render(acct_num):
sign_der = None
with uio.StringIO() as fp:
fp.write('{\n')
with stash.SensitiveValues(secret=alt_secret) as sv:
for name, deriv, fmt in chains.MS_STD_DERIVATIONS:
if fmt == AF_P2SH and acct_num:
continue
dd = deriv.format(coin=chain.b44_cointype, acct_num=acct_num)
if fmt == AF_P2WSH:
sign_der = dd + "/0/0"
node = sv.derive_path(dd)
xp = chain.serialize_public(node, fmt)
fp.write(' "%s_deriv": "%s",\n' % (name, dd))
fp.write(' "%s": "%s",\n' % (name, xp))
xpub = chain.serialize_public(node)
descriptor_template = multisig_descriptor_template(xpub, dd, xfp, fmt)
if descriptor_template is None:
continue
fp.write(' "%s_desc": "%s",\n' % (name, descriptor_template))
fp.write(' "account": "%d",\n' % acct_num)
fp.write(' "xfp": "%s"\n}\n' % xfp)
return fp.getvalue(), sign_der, AF_CLASSIC
from export import export_contents
await export_contents(label, lambda: render(acct), fname_pattern,
force_bbqr=True, is_json=True)
from serializations import ser_compact_size
from desc_utils import Key, read_until
from public_constants import MAX_TR_SIGNERS
class Number:

View File

@ -1,19 +1,18 @@
# (c) Copyright 2018 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# multisig.py - support code for multisig signing and p2sh in general.
# multisig.py - ms coordinator code mostly + some utils
#
import stash, chains, ustruct, ure, uio, sys, ngu, uos, ujson, version
from public_constants import AF_P2WSH, AF_P2WSH_P2SH
from ubinascii import hexlify as b2a_hex
from utils import xfp2str, keypath_to_str
from utils import check_xpub
from ux import ux_show_story, ux_dramatic_pause, ux_clear_keys
from ux import OK, X
from public_constants import AF_P2SH, MAX_SIGNERS, AF_CLASSIC
from utils import xfp2str, extract_cosigner, problem_file_line, get_filesize
from files import CardSlot, CardMissingError, needs_microsd
from ux import ux_show_story, ux_dramatic_pause, ux_enter_number, ux_enter_bip32_index
from public_constants import MAX_SIGNERS
from opcodes import OP_CHECKMULTISIG
from exceptions import FatalPSBTIssue
from glob import settings
from serializations import disassemble
from wallet import BaseStorageWallet
from charcodes import KEY_QR
from desc_utils import Key, KeyOriginInfo
def disassemble_multisig_mn(redeem_script):
@ -27,51 +26,6 @@ def disassemble_multisig_mn(redeem_script):
return M, N
def disassemble_multisig(redeem_script):
# Take apart a standard multisig's redeem/witness script, and return M/N and public keys
# - only for multisig scripts, not general purpose
# - expect OP_1 (pk1) (pk2) (pk3) OP_3 OP_CHECKMULTISIG for 1 of 3 case
# - returns M, N, (list of pubkeys)
# - for very unlikely/impossible asserts, don't document reason; otherwise do.
M, N = disassemble_multisig_mn(redeem_script)
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
assert len(redeem_script) == 1 + (N * 34) + 1 + 1, 'bad len'
# generator function
dis = disassemble(redeem_script)
# expect M value first
ex_M, opcode = next(dis)
assert ex_M == M and opcode is None, 'bad M'
# need N pubkeys
pubkeys = []
for idx in range(N):
data, opcode = next(dis)
assert opcode is None and len(data) == 33, 'data'
assert data[0] == 0x02 or data[0] == 0x03, 'Y val'
pubkeys.append(data)
assert len(pubkeys) == N
# next is N value
ex_N, opcode = next(dis)
assert ex_N == N and opcode is None
# finally, the opcode: CHECKMULTISIG
data, opcode = next(dis)
assert opcode == OP_CHECKMULTISIG
# must have reached end of script at this point
try:
next(dis)
raise AssertionError("too long")
except StopIteration:
# expected, since we're reading past end
pass
return M, N, pubkeys
def make_redeem_script(M, nodes, subkey_idx, bip67=True):
# take a list of BIP-32 nodes, and derive Nth subkey (subkey_idx) and make
# a standard M-of-N redeem script for that. Applies BIP-67 sorting by default.
@ -95,285 +49,8 @@ def make_redeem_script(M, nodes, subkey_idx, bip67=True):
return b''.join(pubkeys)
class MultisigWallet(BaseStorageWallet):
def assert_matching(self, M, N, xfp_paths):
# compare in-memory wallet with details recovered from PSBT
# - xfp_paths must be sorted already
assert (self.M, self.N) == (M, N), "M/N mismatch"
assert len(xfp_paths) == N, "XFP count"
if self.disable_checks: return
assert self.matching_subpaths(xfp_paths), "wrong XFP/derivs"
def has_similar(self):
# check if we already have a saved duplicate to this proposed wallet
# - return (name_change, diff_items, count_similar) where:
# - name_change is existing wallet that has exact match, different name
# - diff_items: text list of similarity/differences
# - count_similar: same N, same xfp+paths
lst = self.get_xfp_paths()
c = self.find_match(self.M, self.N, lst, addr_fmt=self.addr_fmt)
if c:
# All details are same: M/N, paths, addr fmt
if sorted(self.xpubs) != sorted(c.xpubs):
# this also applies to non-BIP-67 type multisig wallets
# multi(2,A,B) is treated as duplicate of multi(2,B,A)
# consensus-wise they are different script/wallet but CC
# don't allow to import one if other already imported
return None, ['xpubs'], 0
elif self.bip67 != c.bip67:
# treat same keys inside different desc multi/sortedmulti as duplicates
# sortedmulti(2,A,B) is considered same as multi(2,A,B) or multi(2,B,A)
# do not allow to import multi if sortedmulti with the same set of keys
# already imported and vice-versa
return None, ["BIP-67 clash"], 1
elif self.name == c.name:
return None, [], 1
else:
return c, ['name'], 0
similar = MultisigWallet.find_candidates(lst)
if not similar:
# no matches, good.
return None, [], 0
# See if the xpubs are changing, which is risky... other differences like
# name are okay.
diffs = set()
for c in similar:
if c.M != self.M:
diffs.add('M differs')
if c.addr_fmt != self.addr_fmt:
diffs.add('address type')
if c.name != self.name:
diffs.add('name')
if c.xpubs != self.xpubs:
diffs.add('xpubs')
return None, diffs, len(similar)
async def export_electrum(self):
# Generate and save an Electrum JSON file.
from export import export_contents
def doit():
rv = dict(seed_version=17, use_encryption=False,
wallet_type='%dof%d' % (self.M, self.N))
ch = self.chain
# the important stuff.
for idx, (xfp, deriv, xpub) in enumerate(self.xpubs):
node = None
if self.addr_fmt != AF_P2SH:
# CHALLENGE: we must do slip-132 format [yz]pubs here when not p2sh mode.
node = ch.deserialize_node(xpub, AF_P2SH); assert node
xp = ch.serialize_public(node, self.addr_fmt)
else:
xp = xpub
rv['x%d/' % (idx+1)] = dict(
hw_type='coldcard', type='hardware',
ckcc_xfp=xfp,
label='Coldcard %s' % xfp2str(xfp),
derivation=deriv, xpub=xp)
# sign export with first p2pkh key
return ujson.dumps(rv), self.get_my_deriv(settings.get('xfp'))+"/0/0", AF_CLASSIC
await export_contents('Electrum multisig wallet', doit,
self.make_fname('el', 'json'), is_json=True)
@classmethod
def import_from_psbt(cls, M, N, xpubs_list):
# given the raw data from PSBT global header, offer the user
# the details, and/or bypass that all and just trust the data.
# - xpubs_list is a list of (xfp+path, binary BIP-32 xpub)
# - already know not in our records.
trust_mode = cls.get_trust_policy()
if trust_mode == TRUST_VERIFY:
# already checked for existing import and wasn't found, so fail
raise FatalPSBTIssue("XPUBs in PSBT do not match any existing wallet")
# build up an in-memory version of the wallet.
# - capture address format based on path used for my leg (if standards compliant)
assert N == len(xpubs_list)
assert 1 <= M <= N <= MAX_SIGNERS, 'M/N range'
my_xfp = settings.get('xfp')
expect_chain = chains.current_chain().ctype
xpubs = []
has_mine = 0
for k, v in xpubs_list:
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
xpub = ngu.codecs.b58_encode(v)
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
expect_chain, my_xfp, cls.disable_checks)
xpubs.append(item)
if is_mine:
has_mine += 1
addr_fmt = cls.guess_addr_fmt(path)
assert has_mine == 1 # 'my key not included'
name = 'PSBT-%d-of-%d' % (M, N)
# this will always create sortedmulti multisig (BIP-67)
# because BIP-174 came years after wide spread acceptance of BIP-67 policy
ms = cls(name, (M, N), xpubs, addr_fmt=addr_fmt or AF_P2SH) # TODO why legacy
# may just keep in-memory version, no approval required, if we are
# trusting PSBT's today, otherwise caller will need to handle UX w.r.t new wallet
return ms, (trust_mode != TRUST_PSBT)
def validate_psbt_xpubs(self, xpubs_list):
# The xpubs provided in PSBT must be exactly right, compared to our record.
# But we're going to use our own values from setup time anyway.
# Check:
# - chain codes match what we have stored already
# - pubkey vs. path will be checked later
# - xfp+path already checked when selecting this wallet
# - some cases we cannot check, so count those for a warning
# Any issue here is a fraud attempt in some way, not innocent.
# But it would not have tricked us and so the attack targets some other signer.
assert len(xpubs_list) == self.N
for k, v in xpubs_list:
xfp, *path = ustruct.unpack_from('<%dI' % (len(k)//4), k, 0)
xpub = ngu.codecs.b58_encode(v)
# cleanup and normalize xpub
tmp = []
is_mine, item = check_xpub(xfp, xpub, keypath_to_str(path, skip=0),
chains.current_chain().ctype, 0, self.disable_checks)
tmp.append(item)
(_, deriv, xpub_reserialized) = tmp[0]
assert deriv # because given as arg
if self.disable_checks:
# allow wrong derivation paths in PSBT; but also allows usage when
# old pre-3.2.1 MS wallet lacks derivation details for all legs
continue
# find in our records.
for (x_xfp, x_deriv, x_xpub) in self.xpubs:
if x_xfp != xfp: continue
# found matching XFP
assert deriv == x_deriv
assert xpub_reserialized == x_xpub, 'xpub wrong (xfp=%s)' % xfp2str(xfp)
break
else:
assert False # not reachable, since we picked wallet based on xfps
async def confirm_import(self):
# prompt them about a new wallet, let them see details and then commit change.
M, N = self.M, self.N
if M == N == 1:
exp = 'The one signer must approve spends.'
elif M == N:
exp = 'All %d co-signers must approve spends.' % N
elif M == 1:
exp = 'Any signature from %d co-signers will approve spends.' % N
else:
exp = '{M} signatures, from {N} possible co-signers, will be required to approve spends.'.format(M=M, N=N)
# Look for duplicate stuff
name_change, diff_items, num_dups = self.has_similar()
is_dup = False
if name_change:
story = 'Update NAME only of existing multisig wallet?'
elif num_dups and isinstance(diff_items, list):
# failures only
story = "Duplicate wallet."
if diff_items:
story += diff_items[0]
else:
story += ' All details are the same as existing!'
is_dup = True
elif diff_items:
# Concern here is overwrite when similar, but we don't overwrite anymore, so
# more of a warning about funny business.
story = '''\
WARNING: This new wallet is similar to an existing wallet, but will NOT replace it. Consider deleting previous wallet first. Differences: \
''' + ', '.join(diff_items)
else:
story = 'Create new multisig wallet?'
derivs, dsum = self.get_deriv_paths()
if not self.bip67 and not is_dup:
# do not need to warn if duplicate, won;t be allowed to import anyways
story += "\nWARNING: BIP-67 disabled! Unsorted multisig - order of keys in descriptor/backup is crucial"
story += '''\n
Wallet Name:
{name}
Policy: {M} of {N}
{exp}
Addresses:
{at}
Derivation:
{dsum}
Press (1) to see extended public keys, '''.format(M=M, N=N, name=self.name, exp=exp, dsum=dsum,
at=self.render_addr_fmt(self.addr_fmt))
if not is_dup:
story += ('%s to approve, %s to cancel.' % (OK, X))
else:
story += '%s to cancel' % X
ux_clear_keys(True)
while 1:
ch = await ux_show_story(story, escape='1')
if ch == '1':
await self.show_detail(verbose=False)
continue
if ch == 'y' and not is_dup:
# save to nvram, may raise WalletOutOfSpace
if name_change:
name_change.delete()
assert self.storage_idx == -1
self.commit()
await ux_dramatic_pause("Saved.", 2)
break
return ch
async def validate_xpub_for_ms(obj, af_str, chain, my_xfp, xpubs):
# Read xpub and validate from JSON received via SD card or BBQr
# - obj => JSON object (mapping)
# - af_str => address format we expect/need
# value in file is BE32, but we want LE32 internally
# - KeyError here handled by caller
xfp = str2xfp(obj['xfp'])
deriv = cleanup_deriv_path(obj[af_str + '_deriv'])
ln = obj.get(af_str)
is_mine, item = check_xpub(xfp, ln, deriv, chain.ctype, my_xfp, xpubs)
xpubs.append(item)
return is_mine
async def ms_coordinator_qr(af_str, my_xfp, chain):
async def ms_coordinator_qr(af_str, my_xfp):
# Scan a number of JSON files from BBQr w/ derive, xfp and xpub details.
#
from ux_q1 import QRScannerInteraction, decode_qr_result, QRDecodeExplained
@ -390,28 +67,32 @@ async def ms_coordinator_qr(af_str, my_xfp, chain):
file_type = 'J'
if file_type == 'J':
try:
import json
return json.loads(data)
return ujson.loads(data)
except:
raise QRDecodeExplained('Unable to decode JSON data')
else:
for line in data.split("\n"):
if len(line) > 112:
l_data = extract_cosigner(line, af_str)
if l_data:
return l_data
if len(line) > 112 and ("pub" in line):
return line.strip()
num_mine = 0
num_files = 0
xpubs = []
keys = []
msg = 'Scan Exported XPUB from Coldcard'
while True:
vals = await QRScannerInteraction().scan_general(msg, convertor, enter_quits=True)
if vals is None:
key = await QRScannerInteraction().scan_general(msg, convertor, enter_quits=True)
if key is None:
break
try:
is_mine = await validate_xpub_for_ms(vals, af_str, chain, my_xfp, xpubs)
if isinstance(key, dict):
k = Key.from_cc_json(key, af_str)
else:
k = Key.from_string(key)
num_mine += k.validate(my_xfp)
keys.append(k)
except KeyError as e:
# random JSON will end up here
msg = "Missing value: %s" % str(e)
@ -421,19 +102,17 @@ async def ms_coordinator_qr(af_str, my_xfp, chain):
msg = "Failure: %s" % str(e)
continue
if is_mine:
num_mine += 1
num_files += 1
msg = "Number of keys scanned: %d" % num_files
return xpubs, num_mine, num_files
return keys, num_mine, num_files
async def ms_coordinator_file(af_str, my_xfp, chain, slot_b=None):
async def ms_coordinator_file(af_str, my_xfp, slot_b=None):
num_mine = 0
num_files = 0
xpubs = []
keys = []
try:
with CardSlot(slot_b=slot_b) as card:
for path in card.get_paths():
@ -471,10 +150,13 @@ async def ms_coordinator_file(af_str, my_xfp, chain, slot_b=None):
if vals:
break
is_mine = await validate_xpub_for_ms(vals, af_str, chain,
my_xfp, xpubs)
if is_mine:
num_mine += 1
if isinstance(vals, dict):
k = Key.from_cc_json(vals, af_str)
else:
k = Key.from_string(vals)
num_mine += k.validate(my_xfp)
keys.append(k)
num_files += 1
@ -490,19 +172,21 @@ async def ms_coordinator_file(af_str, my_xfp, chain, slot_b=None):
await needs_microsd()
return
return xpubs, num_mine, num_files
return keys, num_mine, num_files
def add_own_xpub(chain, acct_num, addr_fmt, secret=None):
# Build out what's required for using master secret (or another
# encoded secret) as a co-signer
deriv = "m/48h/%dh/%dh/%dh" % (chain.b44_cointype, acct_num,
2 if addr_fmt == AF_P2WSH else 1)
deriv = "48h/%dh/%dh/%dh" % (chain.b44_cointype, acct_num,
2 if addr_fmt == AF_P2WSH else 1)
with stash.SensitiveValues(secret=secret) as sv:
node = sv.derive_path(deriv)
the_xfp = sv.get_xfp()
return (the_xfp, deriv, chain.serialize_public(node, AF_P2SH))
the_xfp = xfp2str(sv.get_xfp())
koi = KeyOriginInfo.from_string(the_xfp + "/" + deriv)
node = sv.derive_path(deriv, register=False)
key = Key(node, koi, chain_type=chain.ctype)
return key
async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False, for_ccc=None):
@ -518,21 +202,21 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
my_xfp = settings.get('xfp')
if is_qr:
xpubs, num_mine, num_files = await ms_coordinator_qr(mode, my_xfp, chain)
keys, num_mine, num_files = await ms_coordinator_qr(mode, my_xfp)
else:
xpubs, num_mine, num_files = await ms_coordinator_file(mode, my_xfp, chain)
keys, num_mine, num_files = await ms_coordinator_file(mode, my_xfp)
if CardSlot.both_inserted():
# handle dual slot usage: assumes slot A used by first call above
bxpubs, bnum_mine, bnum_files = await ms_coordinator_file(mode, my_xfp,
chain, True)
xpubs.extend(bxpubs)
bkeys, bnum_mine, bnum_files = await ms_coordinator_file(mode, my_xfp,
slot_b=True)
keys.extend(bkeys)
num_mine += bnum_mine
num_files += bnum_files
# remove dups; easy to happen if you double-tap the export
xpubs = list(set(xpubs))
keys = list(set(keys))
if not xpubs or (len(xpubs) == 1 and num_mine):
if not keys or (len(keys) == 1 and num_mine):
if is_qr:
msg = "No XPUBs scanned. Exit."
else:
@ -554,15 +238,15 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
# problem: above file searching may find xpub export from key C
# (or our master seed, exported) .. we can't add them again,
# since xfp are not unique and that's probably not what they wanted
got_xfps = [a[0], c[0]]
xpubs = [x for x in xpubs if x[0] not in got_xfps]
got_xfps = [a.origin.fingerprint, c.origin.fingerprint]
keys = [k for k in keys if k.origin.fingerprint not in got_xfps]
if not xpubs:
if not keys:
await ux_show_story("Need at least one other co-signer (key B).")
return
# master seed is always key0, key C is key1, k2..kn backup keys
xpubs = [a, c] + xpubs
keys = [a, c] + keys
num_mine += 2
elif not num_mine:
@ -572,10 +256,10 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
if ch == "y":
acct = await ux_enter_bip32_index('Account Number:') or 0
dis.fullscreen("Wait...")
xpubs.append(add_own_xpub(chain, acct, addr_fmt))
keys.append(add_own_xpub(chain, acct, addr_fmt))
num_mine += 1
N = len(xpubs)
N = len(keys)
if (N > MAX_SIGNERS) or (N < 2):
await ux_show_story("Invalid number of signers,min is 2 max is %d." % MAX_SIGNERS)
@ -603,12 +287,18 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
else:
name = 'CC-%d-of-%d' % (M, N)
ms = MultisigWallet(name, (M, N), xpubs, addr_fmt=addr_fmt)
from miniscript import Sortedmulti, Number
from wallet import MiniScriptWallet
from descriptor import Descriptor
desc_obj = Descriptor(miniscript=Sortedmulti(Number(M), *keys),
addr_fmt=addr_fmt)
msc = MiniScriptWallet.from_descriptor_obj(name, desc_obj)
if num_mine:
from auth import NewMiniscriptEnrollRequest, UserAuthorizedAction
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(ms)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(msc)
# menu item case: add to stack
from ux import the_ux
@ -616,7 +306,8 @@ async def ondevice_multisig_create(mode='p2wsh', addr_fmt=AF_P2WSH, is_qr=False,
else:
# we cannot enroll multisig in which we do not participate
# thou we can put descriptor on screen or on SD
await ms.export_wallet_file(descriptor=True, desc_pretty=False)
# cannot sign export if my key not included
await msc.export_wallet_file(sign=False)
async def create_ms_step1(*a, for_ccc=None):
@ -654,6 +345,7 @@ Default is P2WSH addresses (segwit) or press (1) for P2SH-P2WSH.''', escape='1')
try:
return await ondevice_multisig_create(n, f, is_qr, for_ccc=for_ccc)
except Exception as e:
# sys.print_exception(e)
await ux_show_story('Failed to create multisig.\n\n%s\n%s' % (e, problem_file_line(e)),
title="ERROR")
# EOF

View File

@ -631,29 +631,6 @@ class NFCHandler:
else:
raise ValueError(ext)
async def import_multisig_nfc(self, *a):
# user is pushing a file downloaded from another CC over NFC
# - would need an NFC app in between for the sneakernet step
# get some data
def f(m):
if len(m) < 70:
return
m = m.decode()
# multi( catches both multi( and sortedmulti(
if 'pub' in m or "multi(" in m:
return m
winner = await self._nfc_reader(f, 'Unable to find multisig descriptor.')
if winner:
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner)
except Exception as e:
#import sys; sys.print_exception(e)
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_ephemeral_seed_words_nfc(self, *a):
def f(m):
sm = m.decode().strip().split(" ")

View File

@ -208,7 +208,7 @@ class OwnershipCache:
# Find it!
# - returns wallet object, and tuple2 of final 2 subpath components
# - if you start w/ testnet, we'll follow that
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
from glob import dis
ch = chains.current_chain()
@ -308,7 +308,7 @@ class OwnershipCache:
# Provide a simple UX. Called functions do fullscreen, progress bar stuff.
from ux import ux_show_story, show_qr_code
from charcodes import KEY_QR
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
from public_constants import AFC_BECH32, AFC_BECH32M
try:

View File

@ -12,7 +12,7 @@ from uhashlib import sha256
from uio import BytesIO
from sffile import SizerFile
from chains import taptweak, tapleaf_hash
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
from multisig import disassemble_multisig_mn
from exceptions import FatalPSBTIssue, FraudulentChangeOutput
from serializations import ser_compact_size, deser_compact_size, hash160
@ -2679,7 +2679,7 @@ class psbtObject(psbtProxy):
desc = self.active_miniscript.to_descriptor()
if desc.is_basic_multisig:
# we can only finalize multisig inputs from all miniscript set
M, N = desc.miniscript.m_n
M, N = desc.miniscript.m_n()
if len(inp.part_sigs) >= M:
return True
return False
@ -2707,7 +2707,7 @@ class psbtObject(psbtProxy):
assert self.active_miniscript
desc = self.active_miniscript.to_descriptor()
assert desc.is_basic_multisig
M, N = desc.miniscript.m_n
M, N = desc.miniscript.m_n()
if desc.is_sortedmulti:
# BIP-67 easy just sort by public keys
@ -2804,7 +2804,7 @@ class psbtObject(psbtProxy):
assert ssig, 'No signature on input #%d' % in_idx
if inp.is_segwit:
if inp.is_multisig:
if inp.is_miniscript:
if inp.redeem_script:
# p2sh-p2wsh
txi.scriptSig = ser_string(self.get(inp.redeem_script))

View File

@ -15,7 +15,7 @@ from bbqr import b32encode, b32decode
from menu import MenuItem, MenuSystem
from notes import NoteContentBase
from sffile import SFFile
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
# One page github-hosted static website that shows QR based on URL contents pushed by NFC
@ -626,15 +626,9 @@ async def kt_send_psbt(psbt, psbt_len):
# who remains to sign? look at inputs
# all_xfps is set, no need to list one master xfp more than once - assuming CC can sign it all
if psbt.active_multisig:
ms = psbt.active_multisig
all_xfps = {x for x,*p in psbt.active_multisig.get_xfp_paths()}
elif psbt.active_miniscript:
ms = psbt.active_miniscript
all_xfps = {x for x,*p in psbt.active_miniscript.to_descriptor().xfp_paths(skip_unspend_ik=True)}
else:
assert False
assert psbt.active_miniscript
ms = psbt.active_miniscript
all_xfps = {x for x,*p in psbt.active_miniscript.to_descriptor().xfp_paths(skip_unspend_ik=True)}
need = [x for x in psbt.miniscript_xfps_needed() if x in all_xfps]
# maybe it's not really a PSBT where we know the other signers? might be
@ -769,8 +763,8 @@ async def kt_send_file_psbt(*a):
finally:
dis.progress_bar_show(1)
if (not psbt.active_multisig) and (not psbt.active_miniscript):
await ux_show_story("We are not part of this multisig wallet.", "Cannot Teleport PSBT")
if not psbt.active_miniscript:
await ux_show_story("We are not part of this wallet.", "Cannot Teleport PSBT")
return
await kt_send_psbt(psbt, psbt_len=psbt_len)

View File

@ -435,39 +435,6 @@ class USBHandler:
sign_msg(msg, subpath, addr_fmt)
return None
if cmd == 'p2sh':
# show P2SH (probably multisig) address on screen (also provides it back)
# - must provide redeem script, and list of [xfp+path]
from auth import start_show_p2sh_address
if hsm_active and not hsm_active.approve_address_share(is_p2sh=True):
raise HSMDenied
# new multsig goodness, needs mapping from xfp->path and M values
addr_fmt, M, N, script_len = unpack_from('<IBBH', args)
assert addr_fmt & AFC_SCRIPT
assert 1 <= M <= N <= 20
assert 30 <= script_len <= 520
offset = 8
witdeem_script = args[offset:offset+script_len]
offset += script_len
assert len(witdeem_script) == script_len
xfp_paths = []
for i in range(N):
ln = args[offset]
assert 1 <= ln <= 16, 'badlen'
xfp_paths.append(unpack_from('<%dI' % ln, args, offset+1))
offset += (ln*4) + 1
assert offset == len(args)
return b'asci' + start_show_p2sh_address(M, N, addr_fmt, xfp_paths,
witdeem_script)
if cmd == 'show':
# simple cases, older code: text subpath
from auth import usb_show_address
@ -512,7 +479,7 @@ class USBHandler:
if cmd == "msls":
# list all registered miniscript wallet names
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
wallets = [w.name for w in MiniScriptWallet.iter_wallets()]
import ujson
return b'asci' + ujson.dumps(wallets)
@ -520,7 +487,7 @@ class USBHandler:
if cmd == "msdl":
# delete miniscript wallet by its name (unique id)
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
assert len(args) < 40, "len args"
for w in MiniScriptWallet.iter_wallets():
@ -536,7 +503,7 @@ class USBHandler:
if cmd == "msgt":
# takes name and returns descriptor + name json
assert self.encrypted_req, 'must encrypt'
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
assert len(args) < 40, "len args"
for w in MiniScriptWallet.iter_wallets():
@ -552,7 +519,7 @@ class USBHandler:
if hsm_active and not hsm_active.approve_address_share(miniscript=True):
raise HSMDenied
from miniscript import MiniScriptWallet
from wallet import MiniScriptWallet
change, idx, = unpack_from('<II', args)
assert change in (0, 1), "change not bool"

View File

@ -686,87 +686,6 @@ def decode_bip21_text(got):
raise ValueError('not bip-21')
def check_xpub(xfp, xpub, deriv, expect_chain, my_xfp, disable_checks=False):
# Shared code: consider an xpub for inclusion into a wallet
# return T if it's our own key and parsed details in form (xfp, deriv, xpub)
# - deriv can be None, and in very limited cases can recover derivation path
# - could enforce all same depth, and/or all depth >= 1, but
# seems like more restrictive than needed, so "m" is allowed
import stash
from public_constants import AF_P2SH
try:
# Note: addr fmt detected here via SLIP-132 isn't useful
node, chain, _ = parse_extended_key(xpub)
except:
raise AssertionError('unable to parse xpub')
try:
assert node.privkey() == None # 'no privkeys plz'
except ValueError:
pass
if expect_chain == "XRT":
# HACK but there is no difference extended_keys - just bech32 hrp
assert chain.ctype == "XTN"
else:
assert chain.ctype == expect_chain, 'wrong chain'
depth = node.depth()
if depth == 1:
if not xfp:
# allow a shortcut: zero/omit xfp => use observed parent value
xfp = swab32(node.parent_fp())
else:
# generally cannot check fingerprint values, but if we can, do so.
if not disable_checks:
assert swab32(node.parent_fp()) == xfp, 'xfp depth=1 wrong'
assert xfp, 'need fingerprint' # happens if bare xpub given
# In most cases, we cannot verify the derivation path because it's hardened
# and we know none of the private keys involved.
if depth == 1:
# but derivation is implied at depth==1
kn, is_hard = node.child_number()
if is_hard: kn |= 0x80000000
guess = keypath_to_str([kn], skip=0)
if deriv:
if not disable_checks:
assert guess == deriv, '%s != %s' % (guess, deriv)
else:
deriv = guess # reachable? doubt it
assert deriv, 'empty deriv' # or force to be 'm'?
assert deriv[0] == 'm'
# path length of derivation given needs to match xpub's depth
if not disable_checks:
p_len = deriv.count('/')
if p_len:
# only check this for keys that have origin derivation
# originless keys are expected to be blinded
assert p_len == depth, 'deriv %d != %d xpub depth (xfp=%s)' % (
p_len, depth, xfp2str(xfp)
)
else:
# depth can be more than zero here - keys can be blinded
assert xfp == swab32(node.my_fp()), "xpub xfp wrong %s" % xfp2str(xfp)
if xfp == my_xfp:
# it's supposed to be my key, so I should be able to generate pubkey
# - might indicate collision on xfp value between co-signers,
# and that's not supported
with stash.SensitiveValues() as sv:
chk_node = sv.derive_path(deriv)
assert node.pubkey() == chk_node.pubkey(), \
"[%s/%s] wrong pubkey" % (xfp2str(xfp), deriv[2:])
# serialize xpub w/ BIP-32 standard now.
# - this has effect of stripping SLIP-132 confusion away
return xfp == my_xfp, (xfp, deriv, chain.serialize_public(node, AF_P2SH))
def encode_seed_qr(words):
return ''.join('%04d' % bip39.get_word_index(w) for w in words)

View File

@ -950,12 +950,11 @@ class QRScannerInteraction:
proto, addr, args = vals
await ux_visualize_bip21(proto, addr, args)
elif what in ("multi", "minisc"):
elif what == "minisc":
from auth import maybe_enroll_xpub
ms_config, = vals
try:
maybe_enroll_xpub(config=ms_config,
miniscript=False if what == "multi" else None)
maybe_enroll_xpub(config=ms_config)
except Exception as e:
await ux_show_story(
'Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))

View File

@ -2,13 +2,30 @@
#
# wallet.py - A place you find UTXO, addresses and descriptors.
#
import chains
from glob import settings
from stash import SensitiveValues
import ngu, ujson, uio, chains, ure, version, stash
from binascii import hexlify as b2a_hex
from serializations import ser_string
from desc_utils import bip388_wallet_policy_to_descriptor, append_checksum, bip388_validate_policy, Key
from public_constants import AF_P2TR, AF_P2WSH, AF_CLASSIC, AF_P2SH
from menu import MenuSystem, MenuItem, start_chooser
from ux import ux_show_story, ux_confirm, ux_dramatic_pause, OK, X, ux_enter_bip32_index
from files import CardSlot, CardMissingError, needs_microsd
from utils import problem_file_line, xfp2str, to_ascii_printable, swab32, show_single_address
from charcodes import KEY_QR, KEY_CANCEL, KEY_NFC, KEY_ENTER
from glob import settings
# Arbitrary value, not 0 or 1, used to derive a pubkey from preshared xpub in Key Teleport
KT_RXPUBKEY_DERIV = const(20250317)
# PSBT Xpub trust policies
TRUST_VERIFY = const(0)
TRUST_OFFER = const(1)
TRUST_PSBT = const(2)
MAX_BIP32_IDX = (2 ** 31) - 1
class WalletOutOfSpace(RuntimeError):
pass
@ -85,7 +102,7 @@ class MasterSingleSigWallet(WalletABC):
assert 0 <= change_idx <= 1
path += '/%d' % change_idx
with SensitiveValues() as sv:
with stash.SensitiveValues() as sv:
node = sv.derive_path(path)
if count is None: # special case - showing single, ignoring start_idx
@ -110,7 +127,7 @@ class MasterSingleSigWallet(WalletABC):
def render_address(self, change_idx, idx):
# Optimized for a single address.
path = self._path + '/%d/%d' % (change_idx, idx)
with SensitiveValues() as sv:
with stash.SensitiveValues() as sv:
node = sv.derive_path(path)
return self.chain.address(node, self.addr_fmt)
@ -127,20 +144,32 @@ class MasterSingleSigWallet(WalletABC):
return d
class BaseStorageWallet(WalletABC):
key_name = None
class MiniScriptWallet(WalletABC):
skey = "miniscript"
disable_checks = False
def __init__(self, name, desc_tmplt, keys_info, af, ik_u,
desc=None, m_n=None, bip67=None):
assert len(name) <= 20, "name > 20"
def __init__(self):
self.storage_idx = -1
@classmethod
def none_setup_yet(cls):
return '(none setup yet)'
self.name = name
self.desc_tmplt = desc_tmplt
self.keys_info = keys_info
self.desc = desc
self.addr_fmt = af
# internal key unspendable
self.ik_u = ik_u
# below are basic multisig meta
# if m_n is not None, we are dealing with basic multisig
self.m_n = m_n
self.bip67 = bip67
@classmethod
def exists(cls):
# are there any wallets defined?
return bool(settings.get(cls.key_name, []))
return bool(settings.get(cls.skey, []))
@classmethod
def get_all(cls):
@ -150,21 +179,14 @@ class BaseStorageWallet(WalletABC):
@classmethod
def iter_wallets(cls):
# - this is only place we should be searching this list, please!!
lst = settings.get(cls.key_name, [])
lst = settings.get(cls.skey, [])
for idx, rec in enumerate(lst):
yield cls.deserialize(rec, idx)
def serialize(self):
raise NotImplemented
@classmethod
def deserialize(cls, c, idx=-1):
raise NotImplemented
@classmethod
def get_by_idx(cls, nth):
# instance from index number (used in menu)
lst = settings.get(cls.key_name, [])
lst = settings.get(cls.skey, [])
try:
obj = lst[nth]
except IndexError:
@ -178,7 +200,7 @@ class BaseStorageWallet(WalletABC):
# - important that this fails immediately when nvram overflows
obj = self.serialize()
v = settings.get(self.key_name, [])
v = settings.get(self.skey, [])
orig = v.copy()
if not v or self.storage_idx == -1:
# create
@ -188,7 +210,7 @@ class BaseStorageWallet(WalletABC):
# update in place
v[self.storage_idx] = obj
settings.set(self.key_name, v)
settings.set(self.skey, v)
# save now, rather than in background, so we can recover
# from out-of-space situation
@ -197,7 +219,7 @@ class BaseStorageWallet(WalletABC):
except:
# back out change; no longer sure of NVRAM state
try:
settings.set(self.key_name, orig)
settings.set(self.skey, orig)
settings.save()
except: pass # give up on recovery
@ -207,16 +229,824 @@ class BaseStorageWallet(WalletABC):
# remove saved entry
# - important: not expecting more than one instance of this class in memory
assert self.storage_idx >= 0
lst = settings.get(self.key_name, [])
lst = settings.get(self.skey, [])
try:
del lst[self.storage_idx]
if lst:
settings.set(self.key_name, lst)
settings.set(self.skey, lst)
else:
settings.remove_key(self.key_name)
settings.remove_key(self.skey)
settings.save() # actual write
except IndexError: pass
self.storage_idx = -1
def serialize(self):
return (self.name, self.desc_tmplt, self.keys_info,
self.addr_fmt, self.ik_u, self.m_n, self.bip67)
@classmethod
def deserialize(cls, c, idx=-1):
# after deserialization - we lack loaded descriptor object
# we do not need it for everything
name, desc_tmplt, keys_info, af, ik_u, m_n, b67 = c
rv = cls(name, desc_tmplt, keys_info, af, ik_u, m_n=m_n, bip67=b67)
rv.storage_idx = idx
return rv
@classmethod
def get_trust_policy(cls):
which = settings.get('pms', None)
if which is None:
which = TRUST_VERIFY if cls.exists() else TRUST_OFFER
return which
@property
def chain(self):
return chains.current_chain()
@classmethod
def find_match(cls, xfp_paths, addr_fmt=None, M_N=None):
for rv in cls.iter_wallets():
if addr_fmt is not None:
if rv.addr_fmt != addr_fmt:
continue
if M_N:
if not rv.m_n:
continue
if rv.m_n != M_N:
continue
if rv.matching_subpaths(xfp_paths):
return rv
return None
def xfp_paths(self, skip_unspend_ik=False):
if not self.desc:
res = []
for i, k_str in enumerate(self.keys_info):
if not i and self.ik_u and skip_unspend_ik:
continue
k = Key.from_string(k_str)
res.append(k.origin.psbt_derivation())
return res
return self.desc.xfp_paths(skip_unspend_ik=skip_unspend_ik)
def matching_subpaths(self, xfp_paths):
my_xfp_paths = self.to_descriptor().xfp_paths()
if len(xfp_paths) != len(my_xfp_paths):
return False
for x in my_xfp_paths:
prefix_len = len(x)
for y in xfp_paths:
if x == y[:prefix_len]:
break
else:
return False
return True
def subderivation_indexes(self, xfp_paths):
# we already know that they do match
my_xfp_paths = self.to_descriptor().xfp_paths()
res = set()
for x in my_xfp_paths:
prefix_len = len(x)
for y in xfp_paths:
if x == y[:prefix_len]:
to_derive = tuple(y[prefix_len:])
res.add(to_derive)
assert res
if len(res) == 1:
branch, idx = list(res)[0]
else:
branch = [i[0] for i in res]
indexes = set([i[1] for i in res])
assert len(indexes) == 1
idx = list(indexes)[0]
return branch, idx
def get_my_deriv(self, my_xfp):
# lowest public key from lexicographically sorted list is at index 0
mine = self.xpubs_from_xfp(my_xfp)
return mine[0].origin.str_derivation()
def derive_desc(self, xfp_paths):
branch, idx = self.subderivation_indexes(xfp_paths)
derived_desc = self.desc.derive(branch).derive(idx)
return derived_desc
def validate_script_pubkey(self, script_pubkey, xfp_paths, merkle_root=None):
derived_desc = self.derive_desc(xfp_paths)
derived_spk = derived_desc.script_pubkey()
assert derived_spk == script_pubkey, "spk mismatch\n%s\n%s" % (b2a_hex(derived_spk), b2a_hex(script_pubkey))
if merkle_root:
assert derived_desc.tapscript.merkle_root == merkle_root, "psbt merkle root"
return derived_desc
def detail(self):
s = "Wallet Name:\n %s\n\n" % self.name
if self.m_n:
# basic multisig
s += "Policy: %d of %d\n\n" % self.m_n
s += chains.addr_fmt_label(self.addr_fmt)
s += "\n\n" + self.desc_tmplt
return s
async def show_detail(self, story="", allow_import=False):
story += self.detail()
story += "\n\nPress (1) to see extended public keys"
if allow_import:
story += ", OK to approve, X to cancel."
while True:
ch = await ux_show_story(story, escape="1")
if ch == "1":
await self.show_keys()
elif ch != "y":
return None
else:
return True
async def show_keys(self):
msg = ""
for idx, k_str in enumerate(self.keys_info):
if idx:
msg += '\n---===---\n\n'
elif self.addr_fmt == AF_P2TR:
# index 0, taproot internal key
msg += "Taproot internal key:\n\n"
if self.ik_u:
msg += "(provably unspendable)\n\n"
msg += '@%s:\n %s\n\n' % (idx, k_str)
await ux_show_story(msg)
def to_descriptor(self):
if self.desc is None:
# actual descriptor is not loaded, but was asked for
# fill policy - aka storage format - to actual descriptor
import glob
if self.name in glob.DESC_CACHE:
# loaded descriptor from cache
print("to_descriptor CACHE")
self.desc = glob.DESC_CACHE[self.name]
else:
print("loading... policy --> descriptor !!!")
# no need to validate already saved descriptor - was validated upon enroll
self.desc = self._from_bip388_wallet_policy(self.desc_tmplt, self.keys_info,
validate=False)
# cache len always 1
glob.DESC_CACHE = {}
glob.DESC_CACHE[self.name] = self.desc
return self.desc
@staticmethod
def _from_bip388_wallet_policy(desc_template, keys_info, validate=True):
desc_str = bip388_wallet_policy_to_descriptor(
desc_template.replace("/<0;1>/*", "/**"),
keys_info
)
from descriptor import Descriptor
desc_obj = Descriptor.from_string(desc_str, validate=validate)
return desc_obj
@classmethod
def from_bip388_wallet_policy(cls, name, desc_template, keys_info):
bip388_validate_policy(desc_template, keys_info)
desc_obj = cls._from_bip388_wallet_policy(desc_template, keys_info)
msc = cls.from_descriptor_obj(name, desc_obj)
return msc
@classmethod
def from_descriptor_obj(cls, name, desc_obj):
# BIP388 wasn't generated yet - generating from descriptor upon import/enroll
desc_tmplt, keys_info = desc_obj.bip388_wallet_policy()
# self-validation
bip388_validate_policy(desc_tmplt, keys_info)
ik_u = desc_obj.key and desc_obj.key.is_provably_unspendable
af = desc_obj.addr_fmt
m_n = None
bip67 = None
if desc_obj.is_basic_multisig:
m_n = desc_obj.miniscript.m_n()
bip67 = desc_obj.is_sortedmulti
return cls(name, desc_tmplt, keys_info, af, ik_u, desc_obj, m_n, bip67)
@classmethod
def from_file(cls, config, name=None, bip388=False):
from descriptor import Descriptor
if bip388:
# config is JSON wallet policy
wal = cls.from_bip388_wallet_policy(config["name"], config["desc_template"],
config["keys_info"])
else:
if name is None:
desc_obj, cs = Descriptor.from_string(config.strip(), checksum=True)
name = cs
else:
name = to_ascii_printable(name)
desc_obj = Descriptor.from_string(config.strip())
wal = cls.from_descriptor_obj(name, desc_obj)
return wal
def find_duplicates(self):
for rv in self.iter_wallets():
assert self.name != rv.name, ("Miniscript wallet with name '%s'"
" already exists. All wallets MUST"
" have unique names.\n\n" % self.name)
# optimization miniscript vs. multisig & different M/N multisigs
if self.m_n != rv.m_n:
# different M/N
continue
if self.m_n:
# enrolling basic multisig wallet
if self.addr_fmt == rv.addr_fmt and sorted(self.keys_info) == sorted(rv.keys_info):
err = "Duplicate wallet."
if self.bip67 != rv.bip67:
err += " BIP-67 clash."
err += "\n\n"
assert False, err
assert self.desc_tmplt != rv.desc_tmplt \
and self.keys_info != rv.keys_info, ("This wallet is a duplicate "
"of already saved wallet "
"%s.\n\n" % rv.name)
async def confirm_import(self):
nope, yes = (KEY_CANCEL, KEY_ENTER) if version.has_qwerty else ("x", "y")
try:
self.find_duplicates()
story, allow_import = "Create new miniscript wallet?\n\n", True
except AssertionError as e:
story, allow_import = str(e), False
to_save = await self.show_detail(story, allow_import=allow_import)
ch = yes if to_save else nope
if to_save and allow_import:
assert self.storage_idx == -1
self.commit()
import glob
# new wallet was imported - cache descriptor
glob.DESC_CACHE = {}
assert self.desc
glob.DESC_CACHE[self.name] = self.desc
await ux_dramatic_pause("Saved.", 2)
return ch
def yield_addresses(self, start_idx, count, change=False, scripts=False, change_idx=0):
ch = chains.current_chain()
dd = self.to_descriptor().derive(None, change=change)
idx = start_idx
while count:
if idx > MAX_BIP32_IDX:
break
# make the redeem script, convert into address
d = dd.derive(idx)
scr = d.miniscript.compile() if d.miniscript else None
addr = ch.render_address(d.script_pubkey(compiled_scr=scr))
ders = script = None
if scripts:
ders = ["[%s]" % str(k.origin) for k in d.keys]
if d.tapscript:
script = d.tapscript.script_tree()
else:
script = b2a_hex(ser_string(scr)).decode()
yield idx, addr, ders, script
idx += 1
count -= 1
def make_addresses_msg(self, msg, start, n, change=0):
from glob import dis
addrs = []
for idx, addr, *_ in self.yield_addresses(start, n, change=bool(change), scripts=False):
msg += '.../%d =>\n' % idx # just idx, if derivations or scripts needed - export csv
addrs.append(addr)
msg += show_single_address(addr) + '\n\n'
dis.progress_sofar(idx - start + 1, n)
return msg, addrs
def generate_address_csv(self, start, n, change):
yield '"' + '","'.join(
['Index', 'Payment Address']
) + '"\n'
for idx, addr, ders, script in self.yield_addresses(start, n, change=bool(change)):
ln = '%d,"%s"' % (idx, addr)
if ders:
ln += ',"%s","' % script
ln += '","'.join(ders)
ln += '"'
ln += '\n'
yield ln
def to_string(self, checksum=True):
# policy filling - not posible to specify internal/external always multipath export
# only supported from bitcoin-core 29.0
if self.desc_tmplt and self.keys_info:
desc = bip388_wallet_policy_to_descriptor(self.desc_tmplt, self.keys_info)
if checksum:
desc = append_checksum(desc)
return desc
return self.desc.to_string()
def bitcoin_core_serialize(self):
return [{
"desc": self.to_string(), # policy fill
"active": True,
"timestamp": "now",
"range": [0, 100],
}]
async def export_wallet_file(self, core=False, bip388=False, sign=True):
# do not load descriptor - just fill policy
# only with multipath format <0;1>
from glob import NFC, dis
from ux import import_export_prompt
dis.fullscreen('Wait...')
if core:
name = "Bitcoin Core miniscript"
fname_pattern = 'bitcoin-core-%s.txt' % self.name
msg = "importdescriptors cmd"
core_obj = self.bitcoin_core_serialize()
core_str = ujson.dumps(core_obj)
res = "importdescriptors '%s'\n" % core_str
elif bip388:
# policy as JSON
name = "BIP-388 Wallet Policy"
fname_pattern = 'b388-%s.json' % self.name
res = ujson.dumps({"name": self.name,
"desc_template": self.desc_tmplt,
"keys_info": self.keys_info})
else:
name = "Miniscript"
fname_pattern = 'minsc-%s.txt' % self.name
msg = self.name
res = self.to_string()
ch = await import_export_prompt("%s file" % name)
if isinstance(ch, str):
if ch in "3"+KEY_NFC:
await NFC.share_text(res)
elif ch == KEY_QR:
try:
from ux import show_qr_code
await show_qr_code(res, msg=msg)
except:
if version.has_qwerty:
from ux_q1 import show_bbqr_codes
await show_bbqr_codes('U', res, msg)
return
try:
with CardSlot(**ch) as card:
fname, nice = card.pick_filename(fname_pattern)
# do actual write
with open(fname, 'w+') as fp:
fp.write(res)
if sign:
# TODO need function to get my xpub from just policy
# sign with my key at the same path as first address of export
derive = self.get_my_deriv(settings.get('xfp')) + "/0/0"
from msgsign import write_sig_file
h = ngu.hash.sha256s(res.encode())
sig_nice = write_sig_file([(h, fname)], derive, AF_CLASSIC)
msg = '%s file written:\n\n%s' % (name, nice)
if sign:
msg += '\n\n%s signature file written:\n\n%s' % (name, sig_nice)
await ux_show_story(msg)
except CardMissingError:
await needs_microsd()
return
except Exception as e:
await ux_show_story('Failed to write!\n\n%s\n%s' % (e, problem_file_line(e)))
return
def xpubs_from_xfp(self, xfp):
# return list of XPUB's which match xfp
res = []
desc = self.to_descriptor()
for k in desc.keys:
if k.origin and k.origin.cc_fp == xfp:
res.append(k)
elif swab32(k.node.my_fp()) == xfp:
res.append(k)
assert res, "missing xfp %s" % xfp2str(xfp)
# returned is list of keys with corresponding master xfp
# key in list are lexicographically sorted based on their public keys
# lowest public key first
return sorted(res, key=lambda o: o.serialize())
def kt_make_rxkey(self, xfp):
# Derive the receiver's pubkey from preshared xpub and a special derivation
# - also provide the keypair we're using from our side of connection
# - returns 4 byte nonce which is sent un-encrypted, his_pubkey and my_keypair
ri = ngu.random.uniform(1<<28)
# sorted lexicographically, always use the lowest pubkey from the list at index 0
keys = self.xpubs_from_xfp(xfp)
k = keys[0]
k = k.derive(KT_RXPUBKEY_DERIV).derive(ri)
pubkey = k.node.pubkey()
kp = self.kt_my_keypair(ri)
return ri.to_bytes(4, 'big'), pubkey, kp
def kt_my_keypair(self, ri):
# Calc my keypair for sending PSBT files.
#
# sorted lexicographically, always use the lowest pubkey from the list at index 0
keys = self.xpubs_from_xfp(settings.get('xfp'))
subpath = "/%d/%d" % (KT_RXPUBKEY_DERIV, ri)
path = keys[0].origin.str_derivation() + subpath
with stash.SensitiveValues() as sv:
node = sv.derive_path(path)
kp = ngu.secp256k1.keypair(node.privkey())
return kp
@classmethod
def kt_search_rxkey(cls, payload):
# Construct the keypair for to be decryption
# - has to try pubkey each all the unique XFP for all co-signers in all wallets
# - checks checksum of ECDH unwrapped data to see if it's the right one
# - returns session key, decrypted first layer, and XFP of sender
from teleport import decode_step1
# this nonce is part of the derivation path so each txn gets new keys
ri = int.from_bytes(payload[0:4], 'big')
my_xfp = settings.get('xfp')
for msc in cls.iter_wallets():
kp = msc.kt_my_keypair(ri)
for k in msc.to_descriptor().keys:
if k.origin.cc_fp == my_xfp:
continue
kk = k.derive(KT_RXPUBKEY_DERIV).derive(ri)
his_pubkey = kk.node.pubkey()
# if implied session key decodes the checksum, it is right
ses_key, body = decode_step1(kp, his_pubkey, payload[4:])
if ses_key:
return ses_key, body, kk.origin.cc_fp
return None, None, None
async def miniscript_delete(msc):
if not await ux_confirm("Delete miniscript wallet '%s'?\n\nFunds may be impacted." % msc.name):
await ux_dramatic_pause('Aborted.', 3)
return
msc.delete()
await ux_dramatic_pause('Deleted.', 3)
async def miniscript_wallet_delete(menu, label, item):
msc = item.arg
await miniscript_delete(msc)
from ux import the_ux
# pop stack
the_ux.pop()
m = the_ux.top_of_stack()
m.update_contents()
async def miniscript_wallet_detail(menu, label, item):
# show details of single multisig wallet
msc = item.arg
return await msc.show_detail()
async def import_miniscript(*a):
# pick text file from SD card, import as multisig setup file
from actions import file_picker
from ux import import_export_prompt
ch = await import_export_prompt("miniscript wallet file", is_import=True)
if isinstance(ch, str):
if ch == KEY_QR:
await import_miniscript_qr()
elif ch == KEY_NFC:
await import_miniscript_nfc()
return
def possible(filename):
with open(filename, 'rt') as fd:
for ln in fd:
if "sh(" in ln or "wsh(" in ln or "tr(" in ln:
# descriptor import
return True
fn = await file_picker(suffix=['.txt', '.json'], min_size=100,
taster=possible, **ch)
if not fn: return
try:
with CardSlot(**ch) as card:
with open(fn, 'rt') as fp:
data = fp.read()
except CardMissingError:
await needs_microsd()
return
from auth import maybe_enroll_xpub
try:
possible_name = (fn.split('/')[-1].split('.'))[0] if fn else None
maybe_enroll_xpub(config=data, name=possible_name)
except BaseException as e:
await ux_show_story('Failed to import miniscript.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_miniscript_nfc(*a):
from glob import NFC
try:
return await NFC.import_miniscript_nfc()
except Exception as e:
await ux_show_story('Failed to import miniscript.\n\n%s\n%s' % (e, problem_file_line(e)))
async def import_miniscript_qr(*a):
from auth import maybe_enroll_xpub
from ux_q1 import QRScannerInteraction
data = await QRScannerInteraction().scan_text('Scan Miniscript from a QR code')
if not data:
# press pressed CANCEL
return
try:
maybe_enroll_xpub(config=data)
except Exception as e:
await ux_show_story('Failed to import miniscript.\n\n%s\n%s' % (e, problem_file_line(e)))
async def miniscript_wallet_export(menu, label, item):
# create a text file with the details; ready for import to next Coldcard
msc = item.arg[0]
kwargs = item.arg[1]
await msc.export_wallet_file(**kwargs)
async def make_miniscript_wallet_descriptor_menu(menu, label, item):
# descriptor menu
msc = item.arg
if not msc:
return
rv = [
MenuItem('Export', f=miniscript_wallet_export, arg=(msc, {"core": False})),
MenuItem('Bitcoin Core', f=miniscript_wallet_export, arg=(msc, {"core": True})),
MenuItem('BIP-388 Policy', f=miniscript_wallet_export, arg=(msc, {"bip388":True})),
]
return rv
async def make_miniscript_wallet_menu(menu, label, item):
# details, actions on single multisig wallet
msc = MiniScriptWallet.get_by_idx(item.arg)
if not msc: return
rv = [
MenuItem('"%s"' % msc.name, f=miniscript_wallet_detail, arg=msc),
MenuItem('View Details', f=miniscript_wallet_detail, arg=msc),
MenuItem('Delete', f=miniscript_wallet_delete, arg=msc),
MenuItem('Descriptors', menu=make_miniscript_wallet_descriptor_menu, arg=msc),
]
return rv
class MiniscriptMenu(MenuSystem):
@classmethod
def construct(cls):
import version
from menu import ShortcutItem
from bsms import make_ms_wallet_bsms_menu
from multisig import create_ms_step1
if not MiniScriptWallet.exists():
rv = [MenuItem("(none setup yet)")]
else:
rv = []
for msc in MiniScriptWallet.get_all():
rv.append(MenuItem('%s' % msc.name,
menu=make_miniscript_wallet_menu,
arg=msc.storage_idx))
from glob import NFC
rv.append(MenuItem('Import', f=import_miniscript))
rv.append(MenuItem('Export XPUB', f=export_miniscript_xpubs))
rv.append(MenuItem('BSMS (BIP-129)', menu=make_ms_wallet_bsms_menu))
rv.append(MenuItem('Create Airgapped', f=create_ms_step1))
rv.append(MenuItem('Trust PSBT?', f=trust_psbt_menu))
rv.append(MenuItem('Skip Checks?', f=disable_checks_menu))
rv.append(ShortcutItem(KEY_NFC, predicate=lambda: NFC is not None,
f=import_miniscript_nfc))
rv.append(ShortcutItem(KEY_QR, predicate=lambda: version.has_qwerty,
f=import_miniscript_qr))
return rv
def update_contents(self):
# Reconstruct the list of wallets on this dynamic menu, because
# we added or changed them and are showing that same menu again.
tmp = self.construct()
self.replace_items(tmp)
async def make_miniscript_menu(*a):
# list of all multisig wallets, and high-level settings/actions
from pincodes import pa
if pa.is_secret_blank():
await ux_show_story("You must have wallet seed before creating miniscript wallets.")
return
rv = MiniscriptMenu.construct()
return MiniscriptMenu(rv)
def disable_checks_chooser():
ch = ['Normal', 'Skip Checks']
def xset(idx, text):
MiniScriptWallet.disable_checks = bool(idx)
return int(MiniScriptWallet.disable_checks), ch, xset
async def disable_checks_menu(*a):
if not MiniScriptWallet.disable_checks:
ch = await ux_show_story('''\
With many different wallet vendors and implementors involved, it can \
be hard to create a PSBT consistent with the many keys involved. \
With this setting, you can \
disable the more stringent verification checks your Coldcard normally provides.
USE AT YOUR OWN RISK. These checks exist for good reason! Signed txn may \
not be accepted by network.
This settings lasts only until power down.
Press (4) to confirm entering this DANGEROUS mode.
''', escape='4')
if ch != '4': return
start_chooser(disable_checks_chooser)
def psbt_xpubs_policy_chooser():
# Chooser for trust policy
ch = ['Verify Only', 'Offer Import', 'Trust PSBT']
def xset(idx, text):
settings.set('pms', idx)
return MiniScriptWallet.get_trust_policy(), ch, xset
async def trust_psbt_menu(*a):
# show a story then go into chooser
ch = await ux_show_story('''\
This setting controls what the Coldcard does \
with the co-signer public keys (XPUB) that may \
be provided inside a PSBT file. Three choices:
- Verify Only. Do not import the xpubs found, but do \
verify the correct wallet already exists on the Coldcard.
- Offer Import. If it's a new multisig wallet, offer to import \
the details and store them as a new wallet in the Coldcard.
- Trust PSBT. Use the wallet data in the PSBT as a temporary,
multisig wallet, and do not import it. This permits some \
deniability and additional privacy.
When the XPUB data is not provided in the PSBT, regardless of the above, \
we require the appropriate multisig wallet to already exist \
on the Coldcard. Default is to 'Offer' unless a multisig wallet already \
exists, otherwise 'Verify'.''')
if ch == 'x': return
start_chooser(psbt_xpubs_policy_chooser)
async def ms_wallet_electrum_export(menu, label, item):
# create a JSON file that Electrum can use. Challenges:
# - file contains derivation paths for each co-signer to use
# - electrum is using BIP-43 with purpose=48 (purpose48_derivation) to make paths like:
# m/48h/1h/0h/2h
# - above is now called BIP-48
# - other signers might not be coldcards (we don't know)
# solution:
# - when building air-gap, pick address type at that point, and matching path to suit
# - could check path prefix and addr_fmt make sense together, but meh.
ms = item.arg
from actions import electrum_export_story
derivs, dsum = ms.get_deriv_paths()
msg = 'The new wallet will have derivation path:\n %s\n and use %s addresses.\n' % (
dsum, MultisigWallet.render_addr_fmt(ms.addr_fmt) )
if await ux_show_story(electrum_export_story(msg)) != 'y':
return
await ms.export_electrum()
async def export_miniscript_xpubs(*a, xfp=None, alt_secret=None, skip_prompt=False):
# WAS: Create a single text file with lots of docs, and all possible useful xpub values.
# THEN: Just create the one-liner xpub export value they need/want to support BIP-45
# NOW: Export JSON with one xpub per useful address type and semi-standard derivation path
#
# - consumer for this file is supposed to be ourselves, when we build on-device multisig.
# - however some 3rd parties are making use of it as well.
# - used for CCC feature now as well, but result looks just like normal export
#
xfp = xfp2str(xfp or settings.get('xfp', 0))
chain = chains.current_chain()
fname_pattern = 'ccxp-%s.json' % xfp
label = "Multisig XPUB"
if not skip_prompt:
msg = '''\
This feature creates a small file containing \
the extended public keys (XPUB) you would need to join \
a multisig wallet.
Public keys for BIP-48 conformant paths are used:
P2SH-P2WSH:
m/48h/{coin}h/{{acct}}h/1h
P2WSH:
m/48h/{coin}h/{{acct}}h/2h
P2TR:
m/48h/{coin}h/{{acct}}h/3h
{ok} to continue. {x} to abort.'''.format(coin=chain.b44_cointype, ok=OK, x=X)
ch = await ux_show_story(msg)
if ch != "y":
return
acct = await ux_enter_bip32_index('Account Number:') or 0
def render(acct_num):
sign_der = None
with uio.StringIO() as fp:
fp.write('{\n')
with stash.SensitiveValues(secret=alt_secret) as sv:
for name, deriv, fmt in chains.MS_STD_DERIVATIONS:
if fmt == AF_P2SH and acct_num:
continue
dd = deriv.format(coin=chain.b44_cointype, acct_num=acct_num)
if fmt == AF_P2WSH:
sign_der = dd + "/0/0"
node = sv.derive_path(dd)
xp = chain.serialize_public(node, fmt)
fp.write(' "%s_deriv": "%s",\n' % (name, dd))
fp.write(' "%s": "%s",\n' % (name, xp))
xpub = chain.serialize_public(node)
fp.write(' "%s_key_exp": "%s",\n' % (name, "[%s/%s]%s" % (xfp, dd.replace("m/", ""), xpub)))
# descriptor_template = multisig_descriptor_template(xpub, dd, xfp, fmt)
# if descriptor_template is None:
# continue
# fp.write(' "%s_desc": "%s",\n' % (name, descriptor_template))
fp.write(' "account": "%d",\n' % acct_num)
fp.write(' "xfp": "%s"\n}\n' % xfp)
return fp.getvalue(), sign_der, AF_CLASSIC
from export import export_contents
await export_contents(label, lambda: render(acct), fname_pattern,
force_bbqr=True, is_json=True)
# EOF

View File

@ -2665,9 +2665,9 @@ from test_ephemeral import generate_ephemeral_words, import_ephemeral_xprv, goto
from test_ephemeral import ephemeral_seed_disabled_ui, restore_main_seed, confirm_tmp_seed
from test_ephemeral import verify_ephemeral_secret_ui, get_identity_story, get_seed_value_ux, seed_vault_enable
from test_msg import verify_msg_sign_story, sign_msg_from_text, msg_sign_export, sign_msg_from_address
from test_multisig import import_ms_wallet, make_multisig, offer_ms_import, fake_ms_txn
from test_miniscript import offer_minsc_import, get_cc_key, bitcoin_core_signer, import_miniscript
from test_multisig import make_ms_address, clear_ms, make_myself_wallet, import_multisig
from test_multisig import import_ms_wallet, make_multisig, fake_ms_txn
from test_miniscript import offer_minsc_import, get_cc_key, bitcoin_core_signer, import_miniscript, usb_miniscript_get, usb_miniscript_addr
from test_multisig import make_ms_address, make_myself_wallet
from test_notes import need_some_notes, need_some_passwords
from test_nfc import try_sign_nfc, ndef_parse_txn_psbt
from test_se2 import goto_trick_menu, clear_all_tricks, new_trick_pin, se2_gate, new_pin_confirmed

View File

@ -1,13 +0,0 @@
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC.
#
# quickly clear all multisig wallets installed
from glob import settings
from ux import restore_menu
if settings.get('multisig'):
del settings.current['multisig']
settings.save()
print("cleared multisigs")
restore_menu()

View File

@ -45,7 +45,7 @@ def fake_dest_addr(style='p2pkh'):
if style == 'p2wsh':
return bytes([0, 32]) + prandom(32)
if style in ['p2sh', 'p2wsh-p2sh', 'p2wpkh-p2sh']:
if style in ['p2sh', 'p2wsh-p2sh', 'p2sh-p2wsh', 'p2wpkh-p2sh']:
# all equally bogus P2SH outputs
return bytes([0xa9, 0x14]) + prandom(20) + bytes([0x87])

View File

@ -214,11 +214,11 @@ def test_make_backup(multisig, goto_home, pick_menu_item, cap_story, need_keypre
pass_word_quiz, reset_seed_words, import_ms_wallet, get_setting,
reuse_pw, save_pw, settings_set, settings_remove, press_select,
generate_ephemeral_words, set_bip39_pw, verify_backup_file,
check_and_decrypt_backup, restore_backup_cs, clear_ms, seedvault,
check_and_decrypt_backup, restore_backup_cs, clear_miniscript, seedvault,
restore_main_seed, import_ephemeral_xprv, backup_system,
press_cancel, sim_exec, pass_way, garbage_collector, make_big_notes):
# Make an encrypted 7z backup, verify it, and even restore it!
clear_ms()
clear_miniscript()
reset_seed_words()
settings_set("seedvault", int(seedvault))
settings_set("seeds", [] if seedvault else None)

View File

@ -269,7 +269,7 @@ 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,
def test_coordinator_round1(way, encryption_type, M_N, addr_fmt, clear_miniscript, 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, press_cancel):
@ -424,7 +424,7 @@ def test_coordinator_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_
@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_signer_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_home, need_keypress,
def test_signer_round1(way, encryption_type, M_N, addr_fmt, clear_miniscript, goto_home, need_keypress,
cap_story, microsd_path, settings_remove, nfc_read_text, request, settings_get,
make_coordinator_round1, nfc_write_text, microsd_wipe, press_select,
is_q1, pick_menu_item, cap_menu, press_cancel):
@ -581,7 +581,7 @@ def test_signer_round1(way, encryption_type, M_N, addr_fmt, clear_ms, goto_home,
@pytest.mark.parametrize("M_N", [(2,2), (3, 5), (15, 15)])
@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,
def test_coordinator_round2(way, encryption_type, M_N, addr_fmt, auto_collect, clear_miniscript, goto_home,
cap_menu, cap_story, microsd_path, settings_remove, nfc_read_text, request,
settings_get, make_coordinator_round1, make_signer_round1, nfc_write_text,
microsd_wipe, pick_menu_item, press_select, is_q1, need_keypress, press_cancel):
@ -806,7 +806,7 @@ def test_coordinator_round2(way, encryption_type, M_N, addr_fmt, auto_collect, c
@pytest.mark.parametrize("with_checksum", [True, False])
@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,
def test_signer_round2(refuse, way, encryption_type, M_N, addr_fmt, clear_miniscript, goto_home, need_keypress, pick_menu_item,
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):
@ -815,7 +815,7 @@ def test_signer_round2(refuse, way, encryption_type, M_N, addr_fmt, clear_ms, go
virtdisk_path = request.getfixturevalue("virtdisk_path")
virtdisk_wipe()
M, N = M_N
clear_ms()
clear_miniscript()
microsd_wipe()
desc_template, token = make_coordinator_round2(M, N, addr_fmt, encryption_type, way=way, add_checksum=with_checksum)
goto_home()
@ -1184,7 +1184,7 @@ def test_failure_signer_round2(encryption_type, goto_home, press_select, pick_me
@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_integration_signer(encryption_type, M_N, addr_fmt, clear_ms, microsd_wipe, goto_home, pick_menu_item, cap_story,
def test_integration_signer(encryption_type, M_N, addr_fmt, clear_miniscript, microsd_wipe, goto_home, pick_menu_item, cap_story,
press_select, settings_remove, microsd_path, settings_get, cap_menu, use_mainnet,
need_keypress):
# test CC signer full with bsms lib coordinator (test just SD card no need to retest IO paths again - tested above)
@ -1200,7 +1200,7 @@ def test_integration_signer(encryption_type, M_N, addr_fmt, clear_ms, microsd_wi
M, N = M_N
settings_remove(BSMS_SETTINGS)
use_mainnet()
clear_ms()
clear_miniscript()
microsd_wipe()
coordinator = CoordinatorSession(M, N, addr_fmt, et_map[encryption_type])
session_data = coordinator.generate_token_key_pairs()
@ -1332,13 +1332,13 @@ def test_integration_signer(encryption_type, M_N, addr_fmt, clear_ms, microsd_wi
@pytest.mark.parametrize("M_N", [(2,2), (3, 5), (15, 15)])
@pytest.mark.parametrize("addr_fmt", ["p2wsh", "p2sh-p2wsh"])
@pytest.mark.parametrize("cr1_shortcut", [True, False])
def test_integration_coordinator(encryption_type, M_N, addr_fmt, clear_ms, microsd_wipe, goto_home, pick_menu_item,
def test_integration_coordinator(encryption_type, M_N, addr_fmt, clear_miniscript, microsd_wipe, goto_home, pick_menu_item,
cap_story, need_keypress, settings_remove, microsd_path, settings_get, cap_menu,
use_mainnet, cr1_shortcut, press_select):
M, N = M_N
settings_remove(BSMS_SETTINGS)
use_mainnet()
clear_ms()
clear_miniscript()
microsd_wipe()
goto_home()
pick_menu_item('Settings')

View File

@ -513,11 +513,11 @@ def ccc_ms_setup(microsd_path, virtdisk_path, scan_a_qr, is_q1, cap_menu, pick_m
for _ in range(5):
time.sleep(.1)
title, story = cap_story()
if "Create new multisig wallet" in story:
if "Create new miniscript wallet" in story:
break
else:
press_cancel()
assert False, "failed to create ms wallet"
assert False, "failed to create miniscript wallet"
assert f"Policy: 2 of {N}" in story
if is_q1:
@ -546,7 +546,7 @@ def bitcoind_create_watch_only_wallet(pick_menu_item, need_keypress, microsd_pat
pick_menu_item("Descriptors")
pick_menu_item("Bitcoin Core")
res = load_export("sd", label="Bitcoin Core multisig setup", is_json=False)
res = load_export("sd", label="Bitcoin Core miniscript", is_json=False)
res = res.replace("importdescriptors ", "").strip()
r1 = res.find("[")
@ -563,7 +563,7 @@ def bitcoind_create_watch_only_wallet(pick_menu_item, need_keypress, microsd_pat
for obj in res:
assert obj["success"], obj
for _ in range(4):
for _ in range(3):
press_cancel()
return bitcoind_wo
@ -622,7 +622,7 @@ def test_ccc_magnitude(mag_ok, mag, setup_ccc, ccc_ms_setup,
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
if mag_ok:
# always try limit/border value
@ -660,7 +660,7 @@ def test_ccc_whitelist(whitelist_ok, setup_ccc, ccc_ms_setup,
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
whitelist = [
"bcrt1qqca9eefwz8tzn7rk6aumhwhapyf5vsrtrddxxp",
@ -696,7 +696,7 @@ def test_ccc_velocity(velocity_mi, setup_ccc, ccc_ms_setup, bitcoind, settings_s
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
blocks = int(velocity_mi.split()[0])
@ -780,7 +780,7 @@ def test_ccc_warnings(setup_ccc, ccc_ms_setup, bitcoind, settings_set, policy_si
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
whitelist = ["bcrt1qlk39jrclgnawa42tvhu2n7se987qm96qg8v76e",
"2Mxp1Dy2MyR4w36J2VaZhrFugNNFgh6LC1j",
@ -836,12 +836,12 @@ def test_ccc_warnings(setup_ccc, ccc_ms_setup, bitcoind, settings_set, policy_si
def test_maxed_out(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, sim_exec,
bitcoind, settings_get, load_export, press_cancel, restore_main_seed,
bitcoind_create_watch_only_wallet, policy_sign, goto_eph_seed_menu,
pick_menu_item, word_menu_entry, press_select, import_multisig):
pick_menu_item, word_menu_entry, press_select, import_miniscript):
# - maxed out values: 24 words, 25 whitelisted p2wsh values
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
# C mnemonic is 24 words
c_words = "cluster comic depend absent grain circle demand tag pass clock certain strategy lunar bless pulse useful comfort fatigue glove decorate taste allow adult journey".split()
@ -862,8 +862,9 @@ def test_maxed_out(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, sim
pick_menu_item(target_mi) # choose already created multisig
pick_menu_item("Coldcard Export")
ms_conf = load_export("sd", "Coldcard multisig setup", is_json=False)
pick_menu_item("Descriptors")
pick_menu_item("Export")
ms_conf = load_export("sd", "Miniscript", is_json=False)
press_cancel()
# fund CCC multisig
@ -884,7 +885,7 @@ def test_maxed_out(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, sim
time.sleep(0.1)
word_menu_entry(b_words)
press_select()
import_multisig(data=ms_conf)
import_miniscript(data=ms_conf)
press_select() # confirm multisig import
# get rid of last violation - as it is held as global
@ -899,11 +900,11 @@ def test_maxed_out(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, sim
def test_load_and_sign_key_C(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup, sim_exec,
bitcoind_create_watch_only_wallet, pick_menu_item, load_export,
cap_story, press_cancel, bitcoind, policy_sign, restore_main_seed,
verify_ephemeral_secret_ui, word_menu_entry, import_multisig,
verify_ephemeral_secret_ui, word_menu_entry, import_miniscript,
press_select, settings_get, seed_vault, confirm_tmp_seed):
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
settings_set("seedvault", int(seed_vault))
settings_set("seeds", [])
@ -912,8 +913,9 @@ def test_load_and_sign_key_C(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_
bitcoind_wo = bitcoind_create_watch_only_wallet(target_mi)
pick_menu_item(target_mi) # choose already created multisig
pick_menu_item("Coldcard Export")
ms_conf = load_export("sd", "Coldcard multisig setup", is_json=False)
pick_menu_item("Descriptors")
pick_menu_item("Export")
ms_conf = load_export("sd", "Miniscript", is_json=False)
press_cancel()
# fund CCC multisig
@ -942,7 +944,7 @@ def test_load_and_sign_key_C(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_
confirm_tmp_seed(seedvault=seed_vault)
verify_ephemeral_secret_ui(mnemonic=c_words.split(), seed_vault=seed_vault)
import_multisig(data=ms_conf)
import_miniscript(data=ms_conf)
press_select() # confirm multisig import
# get rid of last violation - as it is held as global
@ -974,7 +976,7 @@ def test_ccc_xpub_export(chain, c_num_words, acct, settings_set, load_export, se
goto_home()
settings_set("ccc", None)
settings_set("chain", chain)
settings_set("multisig", [])
settings_set("miniscript", [])
words = None
if isinstance(c_num_words, int):
@ -1018,18 +1020,18 @@ def test_ccc_xpub_export(chain, c_num_words, acct, settings_set, load_export, se
subkey = master.subkey_for_path(xpub_obj[l+"_deriv"])
xpub = subkey.hwif()
assert slip132undo(xpub_obj[l])[0] == xpub
assert xpub in xpub_obj[l+"_desc"]
assert xpub in xpub_obj[l+"_key_exp"]
def test_multiple_multisig_wallets(settings_set, setup_ccc, enter_enabled_ccc, ccc_ms_setup,
bitcoind_create_watch_only_wallet, cap_story, bitcoind,
policy_sign, settings_get, cap_menu, pick_menu_item,
press_select, load_export, offer_ms_import, goto_home):
press_select, load_export, offer_minsc_import, goto_home):
# - 'build 2-of-N' path
goto_home()
settings_set("ccc", None)
settings_set("chain", "XRT")
settings_set("multisig", [])
settings_set("miniscript", [])
words = setup_ccc(c_words=None, mag=2, vel='6 blocks (hour)')
b_keys_0, mi = ccc_ms_setup(N=5)
@ -1091,16 +1093,17 @@ def test_multiple_multisig_wallets(settings_set, setup_ccc, enter_enabled_ccc, c
w_mn, w_name = ami.rsplit(" ", 1)
new_name = "new"
pick_menu_item(ami) # just another ms wallet
pick_menu_item("Coldcard Export")
ms_conf = load_export("sd", label="Coldcard multisig setup", is_json=False)
pick_menu_item("Descriptors")
pick_menu_item("Export")
ms_conf = load_export("sd", "Miniscript", is_json=False)
# try importing duplicate does not work
_, story = offer_ms_import(ms_conf)
assert "Duplicate wallet" in story
_, story = offer_minsc_import(ms_conf)
assert "duplicate of already saved wallet" in story
# try rename
ms_conf = ms_conf.replace(w_name, new_name)
_, story = offer_ms_import(ms_conf)
_, story = offer_minsc_import(ms_conf)
assert "Update NAME only of existing multisig wallet?" in story
press_select()
time.sleep(.1)
@ -1115,7 +1118,7 @@ def test_remove_ccc(settings_set, setup_ccc, ccc_ms_setup, settings_get, policy_
bitcoind_create_watch_only_wallet, bitcoind, goto_home):
goto_home()
settings_set("ccc", None)
settings_set("multisig", [])
settings_set("miniscript", [])
setup_ccc(c_words=None, mag=2, vel='6 blocks (hour)')
_, mi = ccc_ms_setup(N=3)
@ -1124,7 +1127,7 @@ def test_remove_ccc(settings_set, setup_ccc, ccc_ms_setup, settings_get, policy_
ccc_ms_setup(N=5)
assert len(settings_get("multisig")) == 2
assert len(settings_get("miniscript")) == 2
pick_menu_item("Remove CCC") # start remove
time.sleep(.1)
@ -1141,7 +1144,7 @@ def test_remove_ccc(settings_set, setup_ccc, ccc_ms_setup, settings_get, policy_
need_keypress("4")
# multisig wallets are not impacted by removal of ccc
assert len(settings_get("multisig")) == 2
assert len(settings_get("miniscript")) == 2
bitcoind.supply_wallet.sendtoaddress(address=w0.getnewaddress(), amount=5)
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
@ -1157,7 +1160,7 @@ def test_c_key_from_seed_vault(has_candidates, setup_ccc, build_test_seed_vault,
cap_story, press_cancel, enter_enabled_ccc):
goto_home()
settings_set("ccc", None)
settings_set("multisig", [])
settings_set("miniscript", [])
settings_set("seedvault", True)
sv = build_test_seed_vault()
@ -1215,23 +1218,21 @@ def test_c_key_from_seed_vault(has_candidates, setup_ccc, build_test_seed_vault,
@pytest.mark.parametrize("is_bbqr", [True, False])
@pytest.mark.parametrize("N", [3, 15])
def test_ms_setup_cosigner_import(way, ftype, is_bbqr, N, goto_home, settings_set, setup_ccc,
ccc_ms_setup, pick_menu_item, cap_story, is_q1):
ccc_ms_setup, pick_menu_item, is_q1, load_export):
if ((way == "sd") and is_bbqr) or ((not is_q1) and (way == "qr")):
pytest.skip("useless")
goto_home()
settings_set("ccc", None)
settings_set("multisig", [])
settings_set("miniscript", [])
setup_ccc()
keys, target_mi = ccc_ms_setup(N=N, way=way, ftype=ftype, bbqr=is_bbqr)
pick_menu_item(target_mi)
pick_menu_item("Descriptors")
pick_menu_item("View Descriptor")
time.sleep(.1)
_, story = cap_story()
desc = story.split("\n\n")[-1]
pick_menu_item("Export")
desc = load_export("sd", "Miniscript", is_json=False)
for _, obj in keys:
assert f"[{obj['xfp'].lower()}/{obj['p2wsh_deriv'].replace('m/', '')}]{obj['p2wsh']}" in desc

View File

@ -1252,7 +1252,7 @@ def test_add_current_active(reset_seed_words, settings_set, import_ephemeral_xpr
press_cancel, verify_ephemeral_secret_ui,
seed_vault_enable, refuse, press_select, set_bip39_pw,
need_some_notes, need_some_passwords, import_ms_wallet,
restore_main_seed, settings_get, clear_ms):
restore_main_seed, settings_get, clear_miniscript):
ADD_MI = "Add current tmp"
reset_seed_words()
@ -1260,7 +1260,7 @@ def test_add_current_active(reset_seed_words, settings_set, import_ephemeral_xpr
seed_vault_enable(True)
# clear
settings_set("seeds", [])
clear_ms()
clear_miniscript()
settings_set("notes", [])
if not refuse:
@ -1329,7 +1329,7 @@ def test_add_current_active(reset_seed_words, settings_set, import_ephemeral_xpr
@pytest.mark.parametrize('data', SEEDVAULT_TEST_DATA)
def test_temporary_from_backup(multisig, backup_system, import_ms_wallet, get_setting,
data, press_select, cap_story, set_encoded_secret,
reset_seed_words, check_and_decrypt_backup, clear_ms,
reset_seed_words, check_and_decrypt_backup, clear_miniscript,
goto_eph_seed_menu, pick_menu_item, word_menu_entry,
verify_ephemeral_secret_ui, seedvault, settings_set,
seed_vault_enable, confirm_tmp_seed, set_seed_words,
@ -1343,7 +1343,7 @@ def test_temporary_from_backup(multisig, backup_system, import_ms_wallet, get_se
set_encoded_secret(encoded)
settings_set("chain", "XTN")
clear_ms()
clear_miniscript()
if multisig:
import_ms_wallet(15, 15, dev_key=True)

View File

@ -55,63 +55,104 @@ def offer_minsc_import(cap_story, dev, sim_root_dir):
@pytest.fixture
def import_miniscript(goto_home, pick_menu_item, cap_story, need_keypress,
nfc_write_text, press_select, scan_a_qr, press_nfc):
def doit(fname, way="sd", data=None):
goto_home()
pick_menu_item('Settings')
pick_menu_item('Miniscript')
pick_menu_item('Import')
time.sleep(.3)
_, story = cap_story()
if way == "nfc":
if "via NFC" not in story:
pytest.skip("nfc disabled")
def import_miniscript(request, is_q1, need_keypress, offer_minsc_import, press_cancel):
def doit(fname=None, way="sd", data=None, name=None):
assert fname or data
press_nfc()
time.sleep(.1)
if isinstance(data, dict):
data = json.dumps(data)
nfc_write_text(data)
time.sleep(1)
return cap_story()
elif way == "qr":
if isinstance(data, dict):
data = json.dumps(data)
need_keypress(KEY_QR)
try:
scan_a_qr(data)
except:
# always as text - even if it is json
actual_vers, parts = split_qrs(data, 'U', max_version=20)
random.shuffle(parts)
for p in parts:
scan_a_qr(p)
time.sleep(1) # just so we can watch
time.sleep(1)
return cap_story()
if "Press (1) to import miniscript wallet file from SD Card" in story:
# in case Vdisk or NFC is enabled
if fname:
if way == "sd":
need_keypress("1")
microsd_path = request.getfixturevalue("microsd_path")
fpath = microsd_path(fname)
else:
virtdisk_path = request.getfixturevalue("virtdisk_path")
fpath = virtdisk_path(fname)
with open(fpath, 'r') as f:
config = f.read()
else:
config = data
elif way == "vdisk":
if "ress (2)" not in story:
if way in ("usb", None):
return offer_minsc_import(config)
else:
# only get those simulator related fixtures here, to be able to
# use this with real HW
cap_menu = request.getfixturevalue('cap_menu')
cap_story = request.getfixturevalue('cap_story')
goto_home = request.getfixturevalue('goto_home')
press_nfc = request.getfixturevalue('press_nfc')
pick_menu_item = request.getfixturevalue('pick_menu_item')
if "Skip Checks?" not in cap_menu():
# we are not in multisig menu
goto_home()
pick_menu_item("Settings")
pick_menu_item("Miniscript")
time.sleep(.1)
pick_menu_item('Import')
time.sleep(.2)
_, story = cap_story()
if way == "nfc":
if "via NFC" not in story:
press_cancel()
pytest.skip("nfc disabled")
press_nfc()
time.sleep(.1)
if isinstance(config, dict):
config = json.dumps(config)
nfc_write_text = request.getfixturevalue('nfc_write_text')
nfc_write_text(config)
time.sleep(1)
return cap_story()
elif way == "qr":
scan_a_qr = request.getfixturevalue('scan_a_qr')
if isinstance(data, dict):
data = json.dumps(data)
need_keypress(KEY_QR)
try:
scan_a_qr(data)
except:
# always as text - even if it is json
actual_vers, parts = split_qrs(data, 'U', max_version=20)
random.shuffle(parts)
for p in parts:
scan_a_qr(p)
time.sleep(1) # just so we can watch
time.sleep(1)
return cap_story()
if "Press (1) to import miniscript wallet file from SD Card" in story:
# in case Vdisk or NFC is enabled
if way == "sd":
need_keypress("1")
elif way == "vdisk":
if "ress (2)" not in story:
press_cancel()
pytest.xfail(way)
need_keypress("2")
else:
if way != "sd":
pytest.xfail(way)
need_keypress("2")
else:
if way != "sd":
pytest.xfail(way)
if not fname:
microsd_path = request.getfixturevalue("microsd_path")
virtdisk_path = request.getfixturevalue("virtdisk_path")
path_f = microsd_path if way == "sd" else virtdisk_path
fname = (name or "ms_wal") + ".txt"
with open(path_f(fname), "w") as f:
f.write(config)
time.sleep(.5)
pick_menu_item(fname)
time.sleep(.1)
return cap_story()
time.sleep(.3)
pick_menu_item(fname)
time.sleep(.1)
return cap_story()
return doit
@ -1983,12 +2024,10 @@ def test_d_wrapper(addr_fmt, bitcoind, get_cc_key, goto_home, pick_menu_item, ca
"or_d(pk(@A),and_v(v:pkh(@B),after(100)))",
"or_d(multi(2,@A,@C),and_v(v:pkh(@B),after(100)))",
])
def test_import_same_policy_same_keys_diff_order(taproot_ikspendable, minisc,
clear_miniscript, use_regtest,
get_cc_key, bitcoin_core_signer,
offer_minsc_import, cap_menu,
bitcoind, pick_menu_item,
press_select):
def test_import_same_policy_same_keys_diff_order(taproot_ikspendable, minisc, use_regtest,
clear_miniscript, bitcoin_core_signer,
get_cc_key, settings_get, cap_menu,
offer_minsc_import, bitcoind, press_select):
use_regtest()
clear_miniscript()
taproot, ik_spendable = taproot_ikspendable
@ -2032,21 +2071,15 @@ def test_import_same_policy_same_keys_diff_order(taproot_ikspendable, minisc,
title, story = offer_minsc_import(desc1)
assert "Create new miniscript wallet?" in story
press_select()
pick_menu_item("Settings")
pick_menu_item("Miniscript")
m = cap_menu()
m = [i for i in m if not i.startswith("Import")]
assert len(m) == 2
assert len(settings_get("miniscript", [])) == 2
@pytest.mark.parametrize("cs", [True, False])
@pytest.mark.parametrize("way", ["usb", "nfc", "sd", "vdisk"])
def test_import_miniscript_usb_json(use_regtest, cs, way, cap_menu,
clear_miniscript, pick_menu_item,
get_cc_key, bitcoin_core_signer,
offer_minsc_import, bitcoind, microsd_path,
virtdisk_path, import_miniscript, goto_home,
press_select):
def test_import_miniscript_usb_json(use_regtest, cs, way, cap_menu, clear_miniscript, get_cc_key,
bitcoin_core_signer, offer_minsc_import, bitcoind, microsd_path,
virtdisk_path, import_miniscript, goto_home, press_select,
settings_get):
name = "my_minisc"
minsc = f"tr({ranged_unspendable_internal_key()},or_d(multi_a(2,@A,@C),and_v(v:pkh(@B),after(100))))"
use_regtest()
@ -2089,13 +2122,9 @@ def test_import_miniscript_usb_json(use_regtest, cs, way, cap_menu,
assert name in story
press_select()
time.sleep(.2)
goto_home()
pick_menu_item("Settings")
pick_menu_item("Miniscript")
m = cap_menu()
m = [i for i in m if not i.startswith("Import")]
assert len(m) == 1
assert m[0] == name
msc = settings_get("miniscript", [])
assert len(msc) == 1
assert msc[0][0] == name
@pytest.mark.parametrize("config", [
@ -2140,9 +2169,8 @@ def test_unique_name(clear_miniscript, use_regtest, offer_minsc_import,
pick_menu_item("Settings")
pick_menu_item("Miniscript")
m = cap_menu()
m = [i for i in m if not i.startswith("Import")]
assert len(m) == 1
assert m[0] == name
assert m[1] == "Import"
# completely different wallet but with the same name (USB)
yd = json.dumps({"name": name, "desc": y})
@ -2154,9 +2182,8 @@ def test_unique_name(clear_miniscript, use_regtest, offer_minsc_import,
pick_menu_item("Settings")
pick_menu_item("Miniscript")
m = cap_menu()
m = [i for i in m if not i.startswith("Import")]
assert len(m) == 1
assert m[0] == name
assert m[1] == "Import"
goto_home()
fname = f"{name}.txt"

File diff suppressed because it is too large Load Diff

View File

@ -454,7 +454,7 @@ def test_nfc_pushtx(num_outs, chain, enable_nfc, settings_set, settings_remove,
cap_story, cap_screen, has_qwerty, way, try_sign_microsd,
try_sign_nfc, scan_a_qr, need_keypress, press_select,
goto_home, multisig, fake_ms_txn, import_ms_wallet,
clear_ms, try_sign_bbqr):
clear_miniscript, try_sign_bbqr):
# check the NFC push Tx feature, validating the URL's it makes
# - not the UX
# - 100 outs => 5000 or so
@ -463,7 +463,7 @@ def test_nfc_pushtx(num_outs, chain, enable_nfc, settings_set, settings_remove,
from base64 import urlsafe_b64decode
from urllib.parse import urlsplit, parse_qsl, unquote
clear_ms()
clear_miniscript()
settings_set('chain', chain)
enable_nfc()

View File

@ -58,7 +58,7 @@ def test_negative(addr_fmt, testnet, sim_exec):
@pytest.mark.parametrize('from_empty', [ True, False] )
def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
enter_number, press_cancel, settings_set, import_ms_wallet, clear_ms
enter_number, press_cancel, settings_set, import_ms_wallet, clear_miniscript
):
from bech32 import encode as bech32_encode
@ -86,7 +86,7 @@ def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
M, N = 1, 3
expect_name = f'search-test-{addr_fmt}'
clear_ms()
clear_miniscript()
keys = import_ms_wallet(M, N, name=expect_name, accept=1, addr_fmt=addr_fmt_names[addr_fmt])
# iffy: no cosigner index in this wallet, so indicated that w/ path_mapper
@ -103,7 +103,7 @@ def test_positive(addr_fmt, offset, subaccount, chain, from_empty, change_idx,
elif addr_fmt == AF_P2WPKH_P2SH:
menu_item = expect_name = 'P2SH-Segwit'
path = "m/49h/{ct}h/{acc}h"
clear_ms()
clear_miniscript()
elif addr_fmt == AF_P2WPKH:
menu_item = expect_name = 'Segwit P2WPKH'
path = "m/84h/{ct}h/{acc}h"
@ -174,7 +174,7 @@ def test_ux(valid, netcode, method,
sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item,
press_cancel, press_select, settings_set, is_q1, nfc_write, need_keypress,
cap_screen, cap_story, load_shared_mod, scan_a_qr, skip_if_useless_way,
sign_msg_from_address, multisig, import_ms_wallet, clear_ms, verify_qr_address,
sign_msg_from_address, multisig, import_ms_wallet, clear_miniscript, verify_qr_address,
src_root_dir, sim_root_dir
):
skip_if_useless_way(method)
@ -188,7 +188,7 @@ def test_ux(valid, netcode, method,
M, N = 2, 3
expect_name = f'own_ux_test'
clear_ms()
clear_miniscript()
keys = import_ms_wallet(M, N, AF_P2WSH, name=expect_name, accept=1)
# iffy: no cosigner index in this wallet, so indicated that w/ path_mapper
@ -271,7 +271,7 @@ def test_ux(valid, netcode, method,
@pytest.mark.parametrize("af", ["P2SH-Segwit", "Segwit P2WPKH", "Classic P2PKH", "Taproot P2TR", "ms0", "msc0", "msc2"])
def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explorer,
pick_menu_item, need_keypress, sim_exec, clear_ms,
pick_menu_item, need_keypress, sim_exec, clear_miniscript,
import_ms_wallet, press_select, goto_home, nfc_write,
load_shared_mod, load_export_and_verify_signature,
cap_story, load_export, offer_minsc_import, is_q1,
@ -281,7 +281,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo
settings_set('accts', [])
if af == "ms0":
clear_ms()
clear_miniscript()
import_ms_wallet(2, 3, name=af)
press_select() # accept ms import
elif "msc" in af:

View File

@ -515,7 +515,7 @@ def test_ux_countdown_choices(subchoice, expect, xflags, new_trick_pin, new_pin_
# ( 'Blank Coldcard', 'freshly wiped Coldcard', TC_WIPE|TC_BLANK_WALLET, 0 ),
])
def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs, words12,
reset_seed_words, repl, clear_all_tricks, import_ms_wallet, get_setting, clear_ms,
reset_seed_words, repl, clear_all_tricks, import_ms_wallet, get_setting, clear_miniscript,
new_trick_pin, new_pin_confirmed, cap_menu, pick_menu_item, cap_story, need_keypress,
press_select, press_cancel, seed_story_to_words, is_q1, set_seed_words,
stop_after_activated=False,
@ -529,7 +529,7 @@ def test_ux_duress_choices(with_wipe, subchoice, expect, xflags, xargs, words12,
xargs += 1000
# import multisig
clear_ms()
clear_miniscript()
import_ms_wallet(2, 2, dev_key=words12)
press_select()
time.sleep(.1)
@ -879,7 +879,7 @@ def build_duress_wallets(request, seed_vault=False):
# fixtures I need in test_ux_duress_choices
args = {f: request.getfixturevalue(f)
for f in ['reset_seed_words', 'repl', 'clear_all_tricks', 'new_trick_pin', 'clear_ms',
for f in ['reset_seed_words', 'repl', 'clear_all_tricks', 'new_trick_pin', 'clear_miniscript',
'import_ms_wallet', 'get_setting', 'press_select', 'press_cancel', 'is_q1',
'new_pin_confirmed', 'cap_menu', 'pick_menu_item', 'cap_story', 'need_keypress',
'seed_story_to_words', 'set_seed_words']}

View File

@ -479,7 +479,7 @@ def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind
@pytest.mark.bitcoind
@pytest.mark.unfinalized
def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign, end_sign,
decode_psbt_with_bitcoind, offer_ms_import, press_select, clear_ms,
decode_psbt_with_bitcoind, offer_ms_import, press_select, clear_miniscript,
sim_root_dir):
# Use the private key given in BIP 174 and do similar signing
# as the examples.
@ -504,7 +504,7 @@ def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign
xfp = '4F6A0CD9'
config += f'{xfp}: {n1}\n{xfp}: {n2}\n'
clear_ms()
clear_miniscript()
offer_ms_import(config)
time.sleep(.1)
press_select()
@ -3161,9 +3161,9 @@ def test_txout_explorer_op_return(finalize, data, fake_txn, start_sign, cap_stor
def test_low_R_grinding(dev, goto_home, microsd_path, press_select, offer_ms_import,
cap_story, try_sign, reset_seed_words, clear_ms):
cap_story, try_sign, reset_seed_words, clear_miniscript):
reset_seed_words()
clear_ms()
clear_miniscript()
desc = "sh(sortedmulti(2,[6ba6cfd0/45h]tpubD9429UXFGCTKJ9NdiNK4rC5ygqSUkginycYHccqSg5gkmyQ7PZRHNjk99M6a6Y3NY8ctEUUJvCu6iCCui8Ju3xrHRu3Ez1CKB4ZFoRZDdP9/0/*,[747b698e/45h]tpubD97nVL37v5tWyMf9ofh5rznwhh1593WMRg6FT4o6MRJkKWANtwAMHYLrcJFsFmPfYbY1TE1LLQ4KBb84LBPt1ubvFwoosvMkcWJtMwvXgSc/0/*,[7bb026be/45h]tpubD9ArfXowvGHnuECKdGXVKDMfZVGdephVWg8fWGWStH3VKHzT4ph3A4ZcgXWqFu1F5xGTfxncmrnf3sLC86dup2a8Kx7z3xQ3AgeNTQeFxPa/0/*,[0f056943/45h]tpubD8NXmKsmWp3a3DXhbihAYbYLGaRNVdTnr6JoSxxfXYQcmwVtW2hv8QoDwng6JtEonmJoL3cNEwfd2cLXMpGezwZ2vL2dQ7259bueNKj9C8n/0/*))#up0sw2xp"
# PSBT created via fake_ms_txn, grinded in test_ms_sign_myself
psbt_fname = "myself-72sig.psbt"

View File

@ -420,7 +420,7 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite
@pytest.mark.unfinalized
@pytest.mark.parametrize('M', [2, 4])
def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, dev, clear_ms, settings_set,
def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, dev, clear_miniscript, settings_set,
fake_ms_txn, try_sign, bitcoind, cap_story, need_keypress,
cap_menu, pick_menu_item, grab_payload, rx_complete, press_select,
ndef_parse_txn_psbt, press_nfc, nfc_read, settings_get,
@ -430,7 +430,7 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, dev, clear_ms, set
all_out_styles = [af for af in unmap_addr_fmt.keys() if af != "p2tr"]
num_outs = len(all_out_styles)
clear_ms()
clear_miniscript()
use_regtest()
# create a wallet, with 3 bip39 pw's
@ -534,7 +534,7 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, dev, clear_ms, set
assert got_txn
def test_teleport_big_ms(make_myself_wallet, clear_ms, fake_ms_txn, try_sign, cap_story,
def test_teleport_big_ms(make_myself_wallet, clear_miniscript, fake_ms_txn, try_sign, cap_story,
need_keypress, cap_menu, pick_menu_item, grab_payload, rx_complete,
press_select, ndef_parse_txn_psbt, set_master_key, goto_home, press_nfc,
nfc_read, settings_get, settings_set, open_microsd, import_ms_wallet,
@ -542,7 +542,7 @@ def test_teleport_big_ms(make_myself_wallet, clear_ms, fake_ms_txn, try_sign, ca
# define lots of wallets and do teleport from SD disk
clear_ms()
clear_miniscript()
M, N = 2, 15
for i in range(5):
keys = import_ms_wallet(M, N, name=f'ms{i}-test', unique=(i*73), accept=True,
@ -806,7 +806,7 @@ def test_teleport_miniscript_sign(dev, taproot, policy, get_cc_key, bitcoind, us
pick_menu_item(name)
pick_menu_item("Descriptors")
pick_menu_item("Bitcoin Core")
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False, sig_check=False)
text = load_export("sd", label="Bitcoin Core miniscript", is_json=False)
text = text.replace("importdescriptors ", "").strip()
# remove junk
r1 = text.find("[")