better matching

This commit is contained in:
scgbckbone 2025-07-10 15:56:11 +02:00
parent 789b87c33c
commit f8d32a8272
4 changed files with 44 additions and 38 deletions

View File

@ -37,7 +37,8 @@ from public_constants import (
PSBT_GLOBAL_TX_MODIFIABLE, PSBT_GLOBAL_OUTPUT_COUNT, PSBT_GLOBAL_INPUT_COUNT,
PSBT_GLOBAL_FALLBACK_LOCKTIME, PSBT_GLOBAL_TX_VERSION, PSBT_IN_PREVIOUS_TXID,
PSBT_IN_OUTPUT_INDEX, PSBT_IN_SEQUENCE, PSBT_IN_REQUIRED_TIME_LOCKTIME,
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, MAX_PATH_DEPTH, MAX_SIGNERS
PSBT_IN_REQUIRED_HEIGHT_LOCKTIME, MAX_SIGNERS,
AF_P2WSH, AF_P2WSH_P2SH, AF_P2SH, AF_P2TR
)
psbt_tmp256 = bytearray(256)
@ -866,7 +867,7 @@ class psbtInputProxy(psbtProxy):
self.is_p2sh = False
which_key = None
addr_type, addr_or_pubkey, addr_is_segwit = utxo.get_address()
addr_type, addr_or_pubkey, self.is_segwit = utxo.get_address()
if addr_type == "op_return":
self.required_key = None
return
@ -876,12 +877,12 @@ class psbtInputProxy(psbtProxy):
# enough to allow the user to authorize the spend, so fail hard.
raise FatalPSBTIssue('Unhandled scriptPubKey: ' + b2a_hex(addr_or_pubkey).decode())
if addr_is_segwit and not self.is_segwit:
self.is_segwit = True
if addr_type == 'p2sh':
# miniscript input
self.is_p2sh = True
if self.is_segwit:
# we know this just from scriptPubKey --> utxo.get_address()
addr_type = "p2wsh"
# we must have the redeem script already (else fail)
ks = self.witness_script or self.redeem_script
@ -910,7 +911,7 @@ class psbtInputProxy(psbtProxy):
# slight chance of dup xfps, so handle
which_key.add(pubkey)
if not addr_is_segwit and \
if not self.is_segwit and \
len(redeem_script) == 22 and \
redeem_script[0] == 0 and redeem_script[1] == 20:
# it's actually segwit p2pkh inside p2sh
@ -986,13 +987,11 @@ class psbtInputProxy(psbtProxy):
# pubkey provided is just wrong vs. UTXO
raise FatalPSBTIssue('Input #%d: pubkey wrong' % my_idx)
else:
# we don't know how to "solve" this type of input
pass
if self.is_miniscript:
try:
xfp_paths = [item[1:] for item in self.taproot_subpaths.values() if len(item[1:]) > 1]
xfp_paths = [item[1:]
for item in self.taproot_subpaths.values()
if len(item[1:]) > 1]
except AttributeError:
xfp_paths = list(self.subpaths.values())
@ -1000,13 +999,18 @@ class psbtInputProxy(psbtProxy):
if psbt.active_miniscript:
psbt.active_miniscript.matching_subpaths(xfp_paths), "wrong wallet"
else:
wal = MiniScriptWallet.find_match(xfp_paths)
# if we do have actual script at hand, guess M/N for better matching
# basic multisig matching
M, N = disassemble_multisig_mn(self.scriptSig) if self.scriptSig else (None, None)
af = {"p2wsh": AF_P2WSH, "p2sh-p2wsh": AF_P2WSH_P2SH,
"p2sh": AF_P2SH, "p2tr": AF_P2TR}[addr_type]
wal = MiniScriptWallet.find_match(xfp_paths, af, M, N)
if not wal:
raise FatalPSBTIssue('Unknown miniscript wallet')
psbt.active_miniscript = wal
try:
# contains PSBT merkle root verification
# contains PSBT merkle root verification (if taproot)
psbt.active_miniscript.validate_script_pubkey(utxo.scriptPubKey,
xfp_paths, merkle_root)
except BaseException as e:

View File

@ -267,20 +267,23 @@ class MiniScriptWallet(WalletABC):
return chains.current_chain()
@classmethod
def find_match(cls, xfp_paths, addr_fmt=None, M_N=None):
def find_match(cls, xfp_paths, addr_fmt=None, M=None, 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 M and N:
if not rv.m_n:
continue
if rv.m_n != M_N:
m, n = rv.m_n
if m != M or n != N:
continue
if rv.matching_subpaths(xfp_paths):
return rv
return None
def xfp_paths(self, skip_unspend_ik=False):
@ -354,7 +357,8 @@ class MiniScriptWallet(WalletABC):
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
M, N = self.m_n
s += "Policy: %d of %d\n\n" % (M, N)
s += chains.addr_fmt_label(self.addr_fmt)
s += "\n\n" + self.desc_tmplt
@ -487,10 +491,10 @@ class MiniScriptWallet(WalletABC):
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)
else:
if self.desc_tmplt == rv.desc_tmplt and self.keys_info == rv.keys_info:
raise AssertionError ("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")

View File

@ -1000,10 +1000,11 @@ def fake_ms_txn(pytestconfig):
# make a fake txn to supply each of the inputs
# - each input is 1BTC
# addr where the fake money will be stored.
addr, scriptPubKey, script, details = make_ms_address(M, keys, idx=i, bip67=bip67,
violate_script_key_order=violate_script_key_order, path_mapper=path_mapper,
addr_fmt=af,
testnet=net)
addr, scriptPubKey, script, details = make_ms_address(
M, keys, idx=i, bip67=bip67,
violate_script_key_order=violate_script_key_order,
path_mapper=path_mapper, addr_fmt=af, testnet=net
)
print(i, script.hex())
# lots of supporting details needed for p2sh inputs

View File

@ -427,19 +427,20 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, dev, clear_miniscr
txid_from_export_prompt, sim_root_dir):
# IMPORTANT: won't work if you start simulator with --ms flag. Use no args
all_out_styles = [af for af in unmap_addr_fmt.keys() if af != "p2tr"]
num_outs = len(all_out_styles)
num_outs = 4
af = "p2wsh"
clear_miniscript()
use_regtest()
# create a wallet, with 3 bip39 pw's
keys, select_wallet = make_myself_wallet(M, do_import=True)
keys, select_wallet = make_myself_wallet(M, do_import=True, addr_fmt=af)
N = len(keys)
assert M<=N
psbt = fake_ms_txn(15, num_outs, M, keys, segwit_in=True, incl_xpubs=False,
outstyles=all_out_styles, change_outputs=list(range(1,num_outs)))
psbt = fake_ms_txn(15, num_outs, M, keys, inp_addr_fmt=af, incl_xpubs=False,
outstyles=["p2sh-p2wsh", af, af, af],
change_outputs=list(range(1,num_outs)))
with open(f'{sim_root_dir}/debug/myself-before.psbt', 'wb') as f:
f.write(psbt)
@ -537,16 +538,14 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, dev, clear_miniscr
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,
press_cancel):
nfc_read, open_microsd, import_ms_wallet, press_cancel):
# define lots of wallets and do teleport from SD disk
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,
descriptor=False, bip67=True)
keys = import_ms_wallet(M, N, name=f'ms{i}-test', unique=(i*73), accept=True, bip67=True)
# just use last wallet
psbt = fake_ms_txn(1, 1, M, keys)
@ -596,14 +595,12 @@ def test_teleport_big_ms(make_myself_wallet, clear_miniscript, fake_ms_txn, try_
# capture QR+pw to go there
pw, data, qr_raw = grab_payload('E')
tmp_ms = settings_get('multisig')
# switch to that key, receive it
node, = [n for x,n,_ in keys if x == target_xfp]
set_master_key(node.hwif(as_private=True))
# copy over the one MS wallet this xfp was involved in
settings_set('multisig', [tmp_ms[-1]])
import_ms_wallet(M, N, name=f'www', keys=keys, accept=True, bip67=True)
# import and sign
rx_complete(('E', qr_raw), pw, expect_xfp=simulator_fixed_xfp)
@ -651,7 +648,7 @@ def test_teleport_real_ms(dev, fake_ms_txn):
# match the default paths created by CC in airgapped MS wallet creation.
return str_to_path(deriv)
psbt = fake_ms_txn(3, 2, M, keys, fee=10000, outvals=None, segwit_in=False,
psbt = fake_ms_txn(3, 2, M, keys, fee=10000, outvals=None, inp_addr_fmt="p2sh",
outstyles=['p2pkh'], change_outputs=[], incl_xpubs=False,
hack_change_out=False, input_amount=1E8, path_mapper=p2wsh_mapper)