multi-day commit dump
This commit is contained in:
parent
638e7acc55
commit
789b87c33c
@ -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
|
||||
|
||||
@ -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...')
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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,)
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)),
|
||||
]
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(" ")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)))
|
||||
|
||||
886
shared/wallet.py
886
shared/wallet.py
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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']}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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("[")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user