remove MultisigWallet part 1

This commit is contained in:
scgbckbone 2025-07-02 15:10:20 +02:00
parent 3090d220c0
commit 638e7acc55
11 changed files with 271 additions and 1427 deletions

View File

@ -9,7 +9,6 @@ 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 multisig import MultisigWallet
from miniscript import MiniScriptWallet
from uasyncio import sleep_ms
from uhashlib import sha256
@ -199,11 +198,6 @@ class AddressListMenu(MenuSystem):
items.append(MenuItem("Account Number", f=self.change_account))
items.append(MenuItem("Custom Path", menu=self.make_custom))
# if they have MS wallets, add those next
for ms in MultisigWallet.iter_wallets():
if not ms.addr_fmt: continue
items.append(MenuItem(ms.name, f=self.pick_miniscript, arg=ms))
# if they have miniscript wallets, add those next
for msc in MiniScriptWallet.iter_wallets():
items.append(MenuItem(msc.name, f=self.pick_miniscript, arg=msc))
@ -267,7 +261,7 @@ Press (3) if you really understand and accept these risks.
async def show_n_addresses(self, path, addr_fmt, ms_wallet, start=0, n=10, allow_change=True):
# Displays n addresses by replacing {idx} in path format.
# - also for other {account} numbers
# - or multisig case
# - or miniscript case
from glob import dis, NFC
from wallet import MAX_BIP32_IDX

View File

@ -1451,11 +1451,9 @@ class NewMiniscriptEnrollRequest(UserAuthorizedAction):
self.pop_menu()
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_index=None,
miniscript=False):
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 multisig import MultisigWallet
from miniscript import MiniScriptWallet
UserAuthorizedAction.cleanup()
@ -1487,17 +1485,7 @@ def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False, bsms_
# this call will raise on parsing errors, so let them rise up
# and be shown on screen/over usb
if miniscript is None:
# autodetect
try:
msc = MultisigWallet.from_file(config, name=name)
except:
msc = MiniScriptWallet.from_file(config, name=name)
elif miniscript:
msc = MiniScriptWallet.from_file(config, name=name, bip388=bip388)
else:
msc = MultisigWallet.from_file(config, name=name)
msc = MiniScriptWallet.from_file(config, name=name, bip388=bip388)
UserAuthorizedAction.active_request = NewMiniscriptEnrollRequest(msc, bsms_index=bsms_index)

View File

@ -331,8 +331,8 @@ class CCCConfigMenu(MenuSystem):
xfp = CCCFeature.get_xfp()
enc = CCCFeature.get_encoded_secret()
from multisig import export_multisig_xpubs
await export_multisig_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
from miniscript import export_miniscript_xpubs
await export_miniscript_xpubs(xfp=xfp, alt_secret=enc, skip_prompt=True)
async def build_2ofN(self, m, l, i):
count = i.arg

View File

@ -11,7 +11,6 @@ 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 multisig import MultisigWallet
from miniscript import MiniScriptWallet
from ubinascii import hexlify as b2a_hex
from uhashlib import sha256
@ -179,7 +178,7 @@ class ApprovalRule:
# - users: list of authorized users
# - min_users: how many of those are needed to approve
# - local_conf: local user must also confirm w/ code
# - wallet: which multisig/miniscript wallet to restrict to, or '1' for single signer only
# - wallet: which miniscript wallet to restrict to, or '1' for single signer only
# - min_pct_self_transfer: minimum percentage of own input value that must go back to self
# - patterns: list of transaction patterns to check for. Valid values:
# * EQ_NUM_INS_OUTS: the number of inputs and outputs must be equal
@ -196,7 +195,6 @@ class ApprovalRule:
return u
self.index = idx+1
self.ms_type = "multisig"
self.per_period = pop_int(j, 'per_period', 0, MAX_SATS)
self.max_amount = pop_int(j, 'max_amount', 0, MAX_SATS)
self.users = pop_list(j, 'users', check_user)
@ -221,13 +219,10 @@ class ApprovalRule:
# redundant w/ code in pop_int() above
assert 1 <= self.min_users <= len(self.users), "range"
# if specified, 'wallet' must be an existing multisig wallet's name
# if specified, 'wallet' must be an existing miniscript wallet's name
if self.wallet and self.wallet != '1':
ms_names = [ms.name for ms in MultisigWallet.get_all()]
msc_names = [msc.name for msc in MiniScriptWallet.get_all()]
assert self.wallet in (ms_names+msc_names), "unknown wallet: "+self.wallet
if self.wallet in msc_names:
self.ms_type = "miniscript"
assert self.wallet in msc_names, "unknown wallet: " + self.wallet
# patterns must be valid
for p in self.patterns:
@ -273,7 +268,7 @@ class ApprovalRule:
if self.wallet == '1':
rv += ' (singlesig only)'
elif self.wallet:
rv += ' from %s wallet "%s"' % (self.ms_type, self.wallet)
rv += ' from miniscript wallet "%s"' % self.wallet
if self.users:
rv += ' may be authorized by '
@ -314,13 +309,10 @@ class ApprovalRule:
# Does this rule apply to this PSBT file?
if self.wallet:
# rule limited to one wallet
if psbt.active_multisig:
# if multisig signing, might need to match specific wallet name
assert self.wallet == psbt.active_multisig.name, 'wrong multisig wallet'
elif psbt.active_miniscript:
if psbt.active_miniscript:
assert self.wallet == psbt.active_miniscript.name, 'wrong miniscript wallet'
else:
# non multisig, but does this rule apply to all wallets or single-singers
# not miniscript, but does this rule apply to all wallets or single-singers
assert self.wallet == '1', 'singlesig only'
if self.max_amount is not None:
@ -988,8 +980,7 @@ def hsm_status_report():
rv['approval_wait'] = True
rv['users'] = Users.list()
rv['wallets'] = [ms.name for ms in MultisigWallet.get_all()] \
+ [msc.name for msc in MiniScriptWallet.get_all()]
rv['wallets'] = [msc.name for msc in MiniScriptWallet.get_all()]
rv['chain'] = settings.get('chain', 'BTC')

View File

@ -16,11 +16,18 @@ from utils import problem_file_line, xfp2str, to_ascii_printable, swab32, show_s
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):
@ -35,6 +42,15 @@ class MiniScriptWallet(BaseStorageWallet):
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()
@ -538,7 +554,7 @@ async def import_miniscript(*a):
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, miniscript=True)
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)))
@ -557,7 +573,7 @@ async def import_miniscript_qr(*a):
# press pressed CANCEL
return
try:
maybe_enroll_xpub(config=data, miniscript=True)
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)))
@ -610,6 +626,11 @@ class MiniscriptMenu(MenuSystem):
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,
@ -634,6 +655,164 @@ async def make_miniscript_menu(*a):
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)
class Number:
def __init__(self, num):
self.num = num

File diff suppressed because it is too large Load Diff

View File

@ -802,7 +802,7 @@ class NFCHandler:
return await self._nfc_reader(f, 'Unable to find BSMS data in NDEF data')
async def import_miniscript_nfc(self, legacy_multisig=False):
async def import_miniscript_nfc(self):
def f(m):
if len(m) < 70: return
m = m.decode()
@ -816,7 +816,7 @@ class NFCHandler:
from auth import maybe_enroll_xpub
try:
maybe_enroll_xpub(config=winner, miniscript=not legacy_multisig)
maybe_enroll_xpub(config=winner)
except Exception as e:
await ux_show_story('Failed to import.\n\n%s\n%s' % (e, problem_file_line(e)))

View File

@ -208,7 +208,6 @@ class OwnershipCache:
# Find it!
# - returns wallet object, and tuple2 of final 2 subpath components
# - if you start w/ testnet, we'll follow that
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from glob import dis
@ -226,15 +225,11 @@ class OwnershipCache:
if addr_fmt & AFC_SCRIPT:
# multisig or script at least.. must exist already
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=addr_fmt))
msc = [w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == addr_fmt]
possibles.extend(msc)
possibles.extend([w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == addr_fmt])
if addr_fmt == AF_P2SH:
# might look like P2SH but actually be AF_P2WSH_P2SH
possibles.extend(MultisigWallet.iter_wallets(addr_fmt=AF_P2WSH_P2SH))
msc = [w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2WSH_P2SH]
possibles.extend(msc)
possibles.extend([w for w in MiniScriptWallet.iter_wallets() if w.addr_fmt == AF_P2WSH_P2SH])
# Might be single-sig p2wpkh wrapped in p2sh ... but that was a transition
# thing that hopefully is going away, so if they have any multisig wallets,
@ -313,13 +308,12 @@ 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 multisig import MultisigWallet
from miniscript import MiniScriptWallet
from public_constants import AFC_BECH32, AFC_BECH32M
try:
wallet, subpath = OWNERSHIP.search(addr)
is_complex = isinstance(wallet, MultisigWallet) or isinstance(wallet, MiniScriptWallet)
is_complex = isinstance(wallet, MiniScriptWallet)
sp = None
msg = show_single_address(addr)

View File

@ -12,8 +12,8 @@ from uhashlib import sha256
from uio import BytesIO
from sffile import SizerFile
from chains import taptweak, tapleaf_hash
from miniscript import MiniScriptWallet, Key
from multisig import MultisigWallet, disassemble_multisig_mn
from miniscript import MiniScriptWallet
from multisig import disassemble_multisig_mn
from exceptions import FatalPSBTIssue, FraudulentChangeOutput
from serializations import ser_compact_size, deser_compact_size, hash160
from serializations import CTxIn, CTxInWitness, CTxOut, ser_string, COutPoint
@ -483,7 +483,7 @@ class psbtOutputProxy(psbtProxy):
for k, v in self.unknown.items():
wr(k[0], v, k[1:])
def validate(self, out_idx, txo, my_xfp, active_multisig, active_miniscript, parent):
def validate(self, out_idx, txo, my_xfp, active_miniscript, parent):
# Do things make sense for this output?
# NOTE: We might think it's a change output just because the PSBT
@ -535,11 +535,9 @@ class psbtOutputProxy(psbtProxy):
pkh = addr_or_pubkey
if af == 'p2sh':
# P2SH or Multisig output
# Can be both, or either one depending on address type
redeem_script = self.get(self.redeem_script) if self.redeem_script else None
witness_script = self.get(self.witness_script) if self.witness_script else None
if expect_pubkey:
# num_ours == 1 and len(subpaths) == 1, single sig, we only allow p2sh-p2wpkh
@ -562,6 +560,12 @@ class psbtOutputProxy(psbtProxy):
else:
if active_miniscript:
# TODO disable checks
# if MultisigWallet.disable_checks:
# # Without validation, we have to assume all outputs
# # will be taken from us, and are not really change.
# self.is_change = False
# return af
# scriptPubkey can be compared against script that we build - if exact match change
# if not - not change - no need for redeem/witness script
#
@ -574,66 +578,13 @@ class psbtOutputProxy(psbtProxy):
except Exception as e:
raise FraudulentChangeOutput(out_idx, "Change output scriptPubkey: %s" % e)
elif active_multisig:
# Multisig change output, for wallet we're supposed to be a part of.
# - our key must be part of it
# - must look like input side redeem script (same fingerprints)
# - assert M/N structure of output to match any inputs we have signed in PSBT!
# - assert all provided pubkeys are in redeem script, not just ours
# - we get all of that by re-constructing the script from our wallet details
if MultisigWallet.disable_checks:
# Without validation, we have to assume all outputs
# will be taken from us, and are not really change.
self.is_change = False
return af
scr = witness_script or redeem_script
if not scr:
raise FatalPSBTIssue("Missing redeem/witness script for output #%d" % out_idx)
# redeem script must be exactly what we expect
# - pubkeys will be reconstructed from derived paths here
# - BIP-45, BIP-67 rules applied (BIP-67 optional from now - depending on imported descriptor)
# - p2sh-p2wsh needs witness script here, not redeem script value
# - if details provided in output section, must our match multisig wallet
try:
active_multisig.validate_script(scr, subpaths=self.subpaths)
except BaseException as exc:
raise FraudulentChangeOutput(out_idx,
"P2WSH or P2SH change output script: %s" % exc)
else:
# it cannot be change if it doesn't precisely match our multisig setup
# - might be a p2sh output for another wallet that isn't us
# it cannot be change if it doesn't precisely match our miniscript setup
# - might be a output for another wallet that isn't us
# - not fraud, just an output with more details than we need.
self.is_change = False
return af
if is_segwit:
# p2wsh case
# - need witness script and check it's hash against proposed p2wsh value
assert len(addr_or_pubkey) == 32
expect_wsh = ngu.hash.sha256s(witness_script)
if expect_wsh != addr_or_pubkey:
raise FraudulentChangeOutput(out_idx, "P2WSH witness script has wrong hash")
self.is_change = True
return af
if witness_script:
# p2sh-p2wsh case (because it had witness script)
expect_rs = b'\x00\x20' + ngu.hash.sha256s(witness_script)
if redeem_script and expect_rs != redeem_script:
# iff they provide a redeeem script, then it needs to match
# what we expect it to be
raise FraudulentChangeOutput(out_idx,
"P2SH-P2WSH redeem script provided, and doesn't match")
expect_pkh = hash160(expect_rs)
else:
# old BIP-16 style; looks like payment addr
expect_pkh = hash160(redeem_script)
elif af == 'p2pkh':
# input is hash160 of a single public key
assert len(addr_or_pubkey) == 20
@ -680,10 +631,10 @@ class psbtInputProxy(psbtProxy):
blank_flds = (
'unknown', 'utxo', 'witness_utxo', 'sighash', 'redeem_script', 'witness_script',
'fully_signed', 'is_segwit', 'is_multisig', 'is_p2sh', 'num_our_keys',
'fully_signed', 'is_segwit', 'is_p2sh', 'num_our_keys',
'required_key', 'scriptSig', 'amount', 'scriptCode', 'previous_txid',
'prevout_idx', 'sequence', 'req_time_locktime', 'req_height_locktime', 'taproot_key_sig',
'taproot_merkle_root', 'taproot_script_sigs', 'taproot_scripts', "use_keypath", "subpaths",
'taproot_merkle_root', 'taproot_script_sigs', 'taproot_scripts', "subpaths",
"taproot_subpaths", "taproot_internal_key", "is_miniscript",
)
@ -706,7 +657,6 @@ class psbtInputProxy(psbtProxy):
# we can't really learn this until we take apart the UTXO's scriptPubKey
#self.is_segwit = None
#self.is_multisig = None
#self.is_p2sh = False
#self.required_key = None # which of our keys will be used to sign input
@ -812,7 +762,7 @@ class psbtInputProxy(psbtProxy):
# - could consider structure of MofN in p2sh cases
self.fully_signed = (len(self.part_sigs) >= len(self.subpaths))
else:
# No signatures at all yet for this input (typical non multisig)
# No signatures at all yet for this input (typical non miniscript)
self.fully_signed = False
if self.taproot_key_sig:
@ -912,7 +862,6 @@ class psbtInputProxy(psbtProxy):
self.required_key = None
return
self.is_multisig = False
self.is_miniscript = False
self.is_p2sh = False
which_key = None
@ -931,7 +880,7 @@ class psbtInputProxy(psbtProxy):
self.is_segwit = True
if addr_type == 'p2sh':
# multisig input
# miniscript input
self.is_p2sh = True
# we must have the redeem script already (else fail)
@ -948,7 +897,6 @@ class psbtInputProxy(psbtProxy):
which_key, = self.subpaths.keys()
else:
# Assume we'll be signing with any key we know
# - limitation: we cannot be two legs of a multisig (only if CCC feature used)
# - but if partial sig already in place, ignore that one
if not which_key:
which_key = set()
@ -970,14 +918,10 @@ class psbtInputProxy(psbtProxy):
addr = redeem_script[2:22]
self.is_segwit = True
else:
# multiple keys involved, we probably can't do the finalize step
M, N = disassemble_multisig_mn(redeem_script)
if M is None and N is None:
self.is_miniscript = True
else:
self.is_multisig = True
# multiple keys involved
self.is_miniscript = True
if self.witness_script and not self.is_segwit and (self.is_miniscript or self.is_multisig):
if self.witness_script and (not self.is_segwit) and self.is_miniscript:
# bugfix
addr_type = 'p2sh-p2wsh'
self.is_segwit = True
@ -1022,7 +966,6 @@ class psbtInputProxy(psbtProxy):
which_key = xonly_pubkey
# if we find a possibility to spend keypath (internal_key) - we do keypath
# even though script path is available
self.use_keypath = True
break
else:
internal_key = self.get(self.taproot_internal_key)
@ -1047,34 +990,6 @@ class psbtInputProxy(psbtProxy):
# we don't know how to "solve" this type of input
pass
if self.is_multisig:
# We will be signing this input, so
# - find which wallet it is or
# - check it's the right M/N to match redeem script
# - which_key can be empty set, meaning all is already signed
#print("redeem: %s" % b2a_hex(redeem_script))
xfp_paths = list(self.subpaths.values())
xfp_paths.sort()
if not psbt.active_multisig:
# search for multisig wallet
wal = MultisigWallet.find_match(M, N, xfp_paths)
if not wal:
raise FatalPSBTIssue('Unknown multisig wallet')
psbt.active_multisig = wal
else:
# check consistent w/ already selected wallet
psbt.active_multisig.assert_matching(M, N, xfp_paths)
# validate redeem script, by disassembling it and checking all pubkeys
try:
psbt.active_multisig.validate_script(redeem_script, subpaths=self.subpaths)
except BaseException as exc:
# sys.print_exception(exc)
raise FatalPSBTIssue('Input #%d: %s' % (my_idx, exc))
if self.is_miniscript:
try:
xfp_paths = [item[1:] for item in self.taproot_subpaths.values() if len(item[1:]) > 1]
@ -1082,13 +997,14 @@ class psbtInputProxy(psbtProxy):
xfp_paths = list(self.subpaths.values())
xfp_paths.sort()
if not psbt.active_miniscript:
if psbt.active_miniscript:
psbt.active_miniscript.matching_subpaths(xfp_paths), "wrong wallet"
else:
wal = MiniScriptWallet.find_match(xfp_paths)
if not wal:
raise FatalPSBTIssue('Unknown miniscript wallet')
psbt.active_miniscript = wal
assert psbt.active_miniscript
try:
# contains PSBT merkle root verification
psbt.active_miniscript.validate_script_pubkey(utxo.scriptPubKey,
@ -1114,7 +1030,7 @@ class psbtInputProxy(psbtProxy):
#
# Also need this scriptCode for native segwit p2pkh
#
assert not self.is_multisig
assert not self.is_miniscript
self.scriptCode = b'\x19\x76\xa9\x14' + addr + b'\x88\xac'
elif not self.scriptCode:
# Segwit P2SH. We need the witness script to be provided.
@ -1297,9 +1213,8 @@ class psbtObject(psbtProxy):
self.hashValues = None
self.hashScriptPubKeys = None
# this points to a MS wallet, during operation
# - we are only supporting a single multisig wallet during signing
self.active_multisig = None
# this points to a Miniscript wallet, during operation
# - we are only supporting a single miniscript wallet during signing
self.active_miniscript = None
self.warnings = []
@ -1778,7 +1693,7 @@ class psbtObject(psbtProxy):
for idx, txo in self.output_iter():
output = self.outputs[idx]
# perform output validation
af = output.validate(idx, txo, self.my_xfp, self.active_multisig, self.active_miniscript, self)
af = output.validate(idx, txo, self.my_xfp, self.active_miniscript, self)
assert txo.nValue >= 0, "negative output value: o%d" % idx
total_out += txo.nValue
@ -2028,7 +1943,7 @@ class psbtObject(psbtProxy):
# Look at what kind of input this will be, and therefore what
# type of signing will be required, and which key we need.
# - also validates redeem_script when present
# - also finds appropriate multisig wallet to be used
# - also finds appropriate miniscript wallet to be used
inp.determine_my_signing_key(i, utxo, self.my_xfp, self, cosign_xfp)
# iff to UTXO is segwit, then check it's value, and also
@ -2038,8 +1953,6 @@ class psbtObject(psbtProxy):
del utxo
# XXX scan witness data provided, and consider those ins signed if not multisig?
if not foreign:
# no foreign inputs, we can calculate the total input value
assert total_in > 0, "zero value txn"
@ -2078,8 +1991,9 @@ class psbtObject(psbtProxy):
'Some input(s) provided were already completely signed by other parties: ' +
seq_to_str(self.presigned_inputs)))
if MultisigWallet.disable_checks:
self.warnings.append(('Danger', 'Some multisig checks are disabled.'))
# TODO
# if MultisigWallet.disable_checks:
# self.warnings.append(('Danger', 'Some multisig checks are disabled.'))
def calculate_fee(self):
# what miner's reward is included in txn?
@ -2273,7 +2187,7 @@ class psbtObject(psbtProxy):
res = self.check_pubkey_at_path(sv, subpath, pubkey)
if res:
good += 1
# TODO is this needed if output is multisig?
# TODO is this needed if output is multisig? imo not needed note_subpath used is only used with single-sig
OWNERSHIP.note_subpath_used(subpath)
if oup.taproot_subpaths:
@ -2283,7 +2197,7 @@ class psbtObject(psbtProxy):
res = self.check_pubkey_at_path(sv, subpath, xonly_pk, is_xonly=True)
if res:
good += 1
# TODO is this needed if output is miniscript?
# TODO is this needed if output is miniscript? imo not needed note_subpath used is only used with single-sig
OWNERSHIP.note_subpath_used(subpath)
if not good:
@ -2319,7 +2233,7 @@ class psbtObject(psbtProxy):
tr_sh = []
inp.handle_none_sighash()
to_sign = []
if isinstance(inp.required_key, set) and (inp.is_multisig or inp.is_miniscript):
if isinstance(inp.required_key, set) and inp.is_miniscript:
# need to consider a set of possible keys, since xfp may not be unique
for which_key in inp.required_key:
# get node required
@ -2761,11 +2675,14 @@ class psbtObject(psbtProxy):
# double SHA256
return ngu.hash.sha256s(rv.digest())
def multi_input_complete(self, inp):
# raises if input is not multisig or no active_multisig loaded
assert inp.is_multisig
if len(inp.part_sigs) >= self.active_multisig.M:
return True
def miniscript_input_complete(self, inp):
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
if len(inp.part_sigs) >= M:
return True
return False
def is_complete(self):
# Are all the inputs (now) signed?
@ -2776,11 +2693,8 @@ class psbtObject(psbtProxy):
# plus we added some signatures
for i, inp in enumerate(self.inputs):
if i in self.presigned_inputs: continue
if inp.is_miniscript and not inp.use_keypath:
# but we can't combine/finalize miniscript stuff, so will never't be 'final'7
return False
elif inp.is_multisig and self.active_multisig:
if self.multi_input_complete(inp):
elif inp.is_miniscript and self.active_miniscript:
if self.miniscript_input_complete(inp):
signed += 1
elif inp.part_sigs and len(inp.part_sigs) == len(inp.subpaths):
signed += 1
@ -2790,24 +2704,27 @@ class psbtObject(psbtProxy):
return signed == self.num_inputs
def multisig_signatures(self, inp):
assert self.active_multisig
assert self.active_miniscript
desc = self.active_miniscript.to_descriptor()
assert desc.is_basic_multisig
M, N = desc.miniscript.m_n
if self.active_multisig.bip67:
if desc.is_sortedmulti:
# BIP-67 easy just sort by public keys
sigs = [sig for pk, sig in sorted(inp.part_sigs.items())]
else:
# need to respect the order of keys in actual descriptor
sigs = []
for xfp, _, _ in self.active_multisig.xpubs:
for key in desc.keys:
for pk, pth in inp.subpaths.items():
# if xfp matches but pk not in all_sigs -> signer haven't signed
# it is ok in threshold multisig - just skip
if (xfp == pth[0]) and (pk in inp.part_sigs):
if (key.origin.cc_fp == pth[0]) and (pk in inp.part_sigs):
sigs.append(inp.part_sigs[pk])
break
# save space and only provide necessary amount of signatures (smaller tx, less fees)
sigs = sigs[:self.active_multisig.M]
sigs = sigs[:M]
return sigs
def singlesig_signature(self, inp):
@ -2879,8 +2796,8 @@ class psbtObject(psbtProxy):
inp = self.inputs[in_idx]
# first check - if no signature(s) - fail soon
if inp.is_multisig:
assert self.multi_input_complete(inp), 'Incomplete signature set on input #%d' % in_idx
if inp.is_miniscript:
assert self.miniscript_input_complete(inp), 'Incomplete signature set on input #%d' % in_idx
else:
# single signature
ssig = self.singlesig_signature(inp)
@ -2903,7 +2820,7 @@ class psbtObject(psbtProxy):
else:
# insert the new signature(s), assuming fully signed txn.
if inp.is_multisig:
if inp.is_miniscript:
# p2sh multisig (non-segwit)
sigs = self.multisig_signatures(inp)
ss = b"\x00"
@ -2942,7 +2859,7 @@ class psbtObject(psbtProxy):
# can be 65 bytes if sighash != SIGHASH_DEFAULT (0x00)
assert len(inp.taproot_key_sig) in (64, 65)
wit.scriptWitness.stack = [inp.taproot_key_sig]
elif inp.is_multisig:
elif inp.is_miniscript:
sigs = self.multisig_signatures(inp)
wit.scriptWitness.stack = [b""] + sigs + [self.get(inp.witness_script)]
else:

View File

@ -15,7 +15,6 @@ from bbqr import b32encode, b32decode
from menu import MenuItem, MenuSystem
from notes import NoteContentBase
from sffile import SFFile
from multisig import MultisigWallet
from miniscript import MiniScriptWallet
from stash import SensitiveValues, SecretStash, blank_object, bip39_passphrase
@ -252,15 +251,11 @@ async def kt_decode_rx(is_psbt, payload):
ses_key, body = decode_step1(pair, his_pubkey, body)
else:
# Multisig PSBT: will need to iterate over a few wallets and each N-1 possible senders
if (not MultisigWallet.exists()) and (not MiniScriptWallet.exists()):
if not MiniScriptWallet.exists():
await ux_show_story("Incoming PSBT requires miniscript wallet(s) to be already setup, but you have none.")
return
ses_key, body, sender_xfp = MultisigWallet.kt_search_rxkey(payload)
if sender_xfp is None:
ses_key, body, sender_xfp = MiniScriptWallet.kt_search_rxkey(payload)
ses_key, body, sender_xfp = MiniScriptWallet.kt_search_rxkey(payload)
if sender_xfp is not None:
prompt = 'Teleport Password from [%s]' % xfp2str(sender_xfp)

View File

@ -505,7 +505,7 @@ class USBHandler:
# Start an UX interaction, return immediately here
from auth import maybe_enroll_xpub
maybe_enroll_xpub(sf_len=file_len, ux_reset=True, miniscript=True)
maybe_enroll_xpub(sf_len=file_len, ux_reset=True)
return None
@ -571,13 +571,6 @@ class USBHandler:
from auth import start_show_miniscript_address
return b'asci' + start_show_miniscript_address(msc, change, idx)
if cmd == 'msck':
# Quick check to test if we have a wallet already installed.
from multisig import MultisigWallet
M, N, xfp_xor = unpack_from('<3I', args)
return int(MultisigWallet.quick_check(M, N, xfp_xor))
if cmd == 'stxn':
# sign transaction
txn_len, flags, txn_sha = unpack_from('<II32s', args)