finalize foreign single sig outputs from PSBT partial signatures

This commit is contained in:
scgbckbone 2025-04-11 17:38:33 +02:00 committed by doc-hex
parent ef72dc00ae
commit e021fc7317
5 changed files with 87 additions and 11 deletions

View File

@ -33,6 +33,7 @@ This lists the new changes that have not yet been published in a normal release.
- Bugfix: Virtual Disk auto mode stuck on "Reading..." screen
- Bugfix: Do not allow to change Main PIN to value already used as Trick PIN even if
Trick PIN is hidden.
- Bugfix: Finalization of foreign inputs from partial signatures. Thanks Christian Uebber
- Change: `Destroy Seed` also removes all Trick PINs from SE2.

View File

@ -2238,6 +2238,22 @@ class psbtObject(psbtProxy):
sigs = sigs[:self.active_multisig.M]
return sigs
def singlesig_signature(self, inp):
# return signature that we added
# or one signature from partial sigs if input is fully sign
# (i.e. len(part_sigs)>=len(subpaths))
ssig = None
if inp.added_sigs:
# we have added signature to this single sig input
assert len(inp.added_sigs) == 1
ssig = list(inp.added_sigs.items())[0]
elif inp.part_sigs and inp.fully_signed:
assert len(inp.part_sigs) == 1
rv = list(inp.part_sigs.items())[0]
ssig = rv[0], self.get(rv[1])
return ssig
def multisig_xfps_needed(self):
# provide the set of xfp's that still need to sign PSBT
# - used to find which multisig-signer needs to go next
@ -2275,10 +2291,14 @@ class psbtObject(psbtProxy):
fd.write(ser_compact_size(self.num_inputs))
for in_idx, txi in self.input_iter():
inp = self.inputs[in_idx]
# only finalize if input with signatures added by us
assert inp.added_sigs, 'No signature on input #%d' % 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
else:
# single signature
ssig = self.singlesig_signature(inp)
assert ssig, 'No signature on input #%d' % in_idx
if inp.is_segwit:
if inp.is_multisig:
@ -2307,7 +2327,7 @@ class psbtObject(psbtProxy):
txi.scriptSig = ss
else:
pubkey, der_sig = list(inp.added_sigs.items())[0]
pubkey, der_sig = ssig
txi.scriptSig = ser_push_data(der_sig) + ser_push_data(pubkey)
fd.write(txi.serialize())
@ -2329,14 +2349,14 @@ class psbtObject(psbtProxy):
for in_idx, wit in self.input_witness_iter():
inp = self.inputs[in_idx]
if inp.is_segwit and inp.added_sigs:
if inp.is_segwit:
# put in new sig: wit is a CTxInWitness
assert not wit.scriptWitness.stack, 'replacing non-empty?'
if inp.is_multisig:
sigs = self.multisig_signatures(inp)
wit.scriptWitness.stack = [b""] + sigs + [self.get(inp.witness_script)]
else:
pubkey, der_sig = list(inp.added_sigs.items())[0]
pubkey, der_sig = self.singlesig_signature(inp)
assert pubkey[0] in {0x02, 0x03} and len(pubkey) == 33, "bad v0 pubkey"
wit.scriptWitness.stack = [der_sig, pubkey]

View File

@ -758,7 +758,7 @@ def test_big_txn(num_in, num_out, dev, quick_start_hsm, hsm_status, is_simulator
attempt_psbt(psbt)
@pytest.mark.veryslow
@pytest.mark.manual
def test_multiple_signings(dev, quick_start_hsm, is_simulator,
attempt_psbt, fake_txn, load_hsm_users,
auth_user):
@ -774,7 +774,7 @@ def test_multiple_signings(dev, quick_start_hsm, is_simulator,
attempt_psbt(psbt)
@pytest.mark.veryslow
@pytest.mark.manual
@pytest.mark.parametrize("cc_first", [True, False])
@pytest.mark.parametrize("M_N", [(2,3), (3,5), (15,15)])
def test_multiple_signings_multisig(cc_first, M_N, dev, quick_start_hsm,

View File

@ -1548,7 +1548,7 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev
assert is_complete
@pytest.mark.parametrize('addr_fmt', ['p2wsh', 'p2sh-p2wsh'])
@pytest.mark.parametrize('acct_num', [ 0, None, 4321])
@pytest.mark.parametrize('acct_num', [None, 4321])
@pytest.mark.parametrize('M_N', [(2,3), (8,14)])
@pytest.mark.parametrize('way', ["sd", "qr"])
@pytest.mark.parametrize('incl_self', [True, False, None])

View File

@ -16,7 +16,7 @@ from helpers import B2A, fake_dest_addr, parse_change_back, addr_from_display_fo
from helpers import xfp2str, seconds2human_readable, hash160
from msg import verify_message
from bip32 import BIP32Node
from constants import ADDR_STYLES, ADDR_STYLES_SINGLE, SIGHASH_MAP
from constants import ADDR_STYLES, ADDR_STYLES_SINGLE, SIGHASH_MAP, simulator_fixed_xfp
from txn import *
from ctransaction import CTransaction, CTxOut, CTxIn, COutPoint
from ckcc_protocol.constants import STXN_VISUALIZE, STXN_SIGNED
@ -2070,8 +2070,8 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev,
return doit
# TODO Locktime test MUST be run with --psbt2 flag on and off
# pytest test_sign.py -k locktime {--psbt2,}
# TODO Sighash test MUST be run with --psbt2 flag on and off
# pytest test_sign.py -k sighash {--psbt2,}
@pytest.mark.bitcoind
@pytest.mark.parametrize("addr_fmt", ["legacy", "p2sh-segwit", "bech32"])
@ -3081,4 +3081,59 @@ def test_mk4_done_signing_infinite_loop(goto_home, try_sign, fake_txn, enable_hw
if had_vdisk:
enable_hw_ux("vdisk")
@pytest.mark.bitcoind
def test_finalize_with_foreign_inputs(bitcoind, bitcoind_d_sim_watch, start_sign, end_sign,
cap_story, try_sign_microsd):
# foreign inputs that have partial sigs filled
# we still do not care about final_scriptsig & final_scriptwitness PSBT fields
dest_address = bitcoind.supply_wallet.getnewaddress()
alice = bitcoind.create_wallet(wallet_name="alice")
bob = bitcoind.create_wallet(wallet_name="bob")
cc = bitcoind_d_sim_watch
alice_addr = alice.getnewaddress()
bob_addr = bob.getnewaddress()
cc_addr = cc.getnewaddress()
# fund all addresses
for addr in (alice_addr, bob_addr, cc_addr):
bitcoind.supply_wallet.sendtoaddress(addr, 2.0)
# mine above sends
bitcoind.supply_wallet.generatetoaddress(1, bitcoind.supply_wallet.getnewaddress())
psbt_list = []
for w in (alice, bob, cc):
assert w.listunspent()
psbt = w.walletcreatefundedpsbt([], [{dest_address: 1.0}], 0, {"fee_rate": 20})["psbt"]
psbt_list.append(psbt)
# join PSBTs to one
the_psbt = bitcoind.supply_wallet.joinpsbts(psbt_list)
# bitcoin core would just fill finalscriptwitness, we need partial signatures
# just add dummy signatures and remove
pp = BasicPSBT().parse(base64.b64decode(the_psbt))
for i in pp.inputs:
assert len(i.bip32_paths) == 1 # single sigs
der = list(i.bip32_paths.values())[0]
if der[:4].hex().upper() == xfp2str(simulator_fixed_xfp):
# our key
continue
pubkey = list(i.bip32_paths.keys())[0]
assert not i.part_sigs # empty
i.part_sigs[pubkey] = os.urandom(71) # dummy sig
# USB works and our signature is added (but only if we do not finalize)
psbt = pp.as_bytes()
start_sign(psbt)
signed = end_sign(accept=True)
assert signed != psbt
for i in BasicPSBT().parse(signed).inputs:
assert i.part_sigs
try_sign_microsd(psbt, finalize=True, accept=True)
title, story = cap_story()
assert title == "PSBT Signed"
assert "Finalized transaction (ready for broadcast)" in story
# EOF