remove MultisigWallet part 1
This commit is contained in:
parent
3090d220c0
commit
638e7acc55
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
1239
shared/multisig.py
1239
shared/multisig.py
File diff suppressed because it is too large
Load Diff
@ -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)))
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
191
shared/psbt.py
191
shared/psbt.py
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user