Combines signed PSBT files

This commit is contained in:
Peter D. Gray 2019-06-19 09:47:26 -04:00
parent cc5c0532e5
commit 1bde362ddb
No known key found for this signature in database
GPG Key ID: F0E6CC6AFC16CF7B
3 changed files with 93 additions and 43 deletions

View File

@ -6,7 +6,7 @@ from base64 import b64decode
from binascii import a2b_hex, b2a_hex
from struct import pack, unpack
from electrum.transaction import Transaction, multisig_script
from electrum.transaction import Transaction, multisig_script, parse_redeemScript_multisig
from electrum.logging import get_logger
from electrum.wallet import Standard_Wallet, Multisig_Wallet, Wallet
@ -288,5 +288,27 @@ def build_psbt(tx: Transaction, wallet: Wallet):
return tx.raw_psbt
def combine_psbt(tx: Transaction, psbt):
# Take new signatures from PSBT, and merge into on-going in-memory transaction.
# - "we trust everyone here"
count = 0
for inp_idx, inp in enumerate(psbt.inputs):
# need to map from pubkey to signing position in redeem script
M, N, _, pubkeys, _ = parse_redeemScript_multisig(inp.redeem_script)
#assert (M, N) == (wallet.m, wallet.n)
for sig_pk in inp.part_sigs:
pk_pos = pubkeys.index(sig_pk.hex())
tx.add_signature_to_txin(inp_idx, pk_pos, inp.part_sigs[sig_pk].hex())
count += 1
assert count, "unable to add any partial sigs"
# reset serialization of TX
tx.raw = tx.serialize()
return count
# EOF

View File

@ -10,7 +10,7 @@ from electrum.bip32 import BIP32Node, InvalidMasterKeyVersionBytes
from electrum.i18n import _
from electrum.plugin import Device, hook
from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey, Xpub
from electrum.transaction import Transaction, multisig_script, parse_redeemScript_multisig
from electrum.transaction import Transaction, multisig_script
from electrum.wallet import Standard_Wallet, Multisig_Wallet, Wallet
from electrum.crypto import hash_160
from electrum.util import bfh, bh2u, versiontuple, UserFacingException
@ -22,7 +22,7 @@ from ..hw_wallet import HW_PluginBase
from ..hw_wallet.plugin import LibraryFoundButUnusable, only_hook_if_libraries_available
from .basic_psbt import BasicPSBT
from .build_psbt import build_psbt, xfp2str, unpacked_xfp_path
from .build_psbt import build_psbt, xfp2str, unpacked_xfp_path, combine_psbt
_logger = get_logger(__name__)
@ -258,6 +258,7 @@ class Coldcard_KeyStore(Hardware_KeyStore):
self.ux_busy = False
# for multisig I need to know what wallet this keystore is part of
# this is captured by hooling make_unsigned_transaction
self.my_wallet = None
# Seems like only the derivation path and resulting **derived** xpub is stored in
@ -386,12 +387,17 @@ class Coldcard_KeyStore(Hardware_KeyStore):
client = self.get_client()
from pprint import pprint
for n,i in enumerate(tx.inputs()):
print('[%d]: ' % n, end='')
pprint(i)
assert client.dev.master_fingerprint == self.ckcc_xfp
# makes PSBT required
raw_psbt = build_psbt(tx, self.my_wallet)
cc_finalize = not (type(wallet) is Multisig_Wallet)
cc_finalize = not (type(self.my_wallet) is Multisig_Wallet)
try:
try:
@ -430,33 +436,12 @@ class Coldcard_KeyStore(Hardware_KeyStore):
else:
# apply partial signatures back into txn
psbt = BasicPSBT()
psbt.parse(raw_resp, self.get_label())
psbt.parse(raw_resp, client.label())
self.merge_psbt(tx, psbt, wallet)
combine_psbt(tx, psbt)
def merge_psbt(self, tx: Transaction, psbt: BasicPSBT, wallet: Wallet):
# Take new signatures from PSBT, and merge into on-going in-memory transaction.
# - "we trust everyone here"
count = 0
for inp_idx, inp in enumerate(psbt.inputs):
# need to map from pubkey to signing position in redeem script
M, N, _, pubkeys, _ = parse_redeemScript_multisig(inp.redeem_script)
assert (M, N) == (wallet.M, wallet.N)
for sig_pk in inp.part_sigs:
pk_pos = pubkeys.find(sig_pk)
assert pk_pos >= 0, "unknown pubkey?"
tx.add_signature_to_txin(inp_idx, pk_pos, sig)
count += 1
assert count, "unable to add any partial sigs"
# reset / update objs
tx.raw = tx.serialize()
tx.raw_psbt = None
return count
# caller's logic looks at tx now and if it's sufficiently signed,
# will send it if that's the user's intent.
@staticmethod
def _encode_txin_type(txin_type):
@ -686,12 +671,14 @@ class ColdcardPlugin(HW_PluginBase):
keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device))
return
# overloads HW_PluginBase.load_wallet, which we need
def XXX_load_wallet(self, wallet, window):
# Hook runs when a specific wallet is openned okay; also maybe a new one?
# - capture wallet containing keystore
@hook
def make_unsigned_transaction(self, wallet, tx):
# PROBLEM: wallet.sign_transaction() does not pass in the wallet to the individual
# keystores, and we need to know about our co-signers at that time.
# - capture wallet containing each keystore early in the process
for ks in wallet.get_keystores():
if type(ks) == Coldcard_KeyStore:
ks.my_wallet = wallet
if not ks.my_wallet:
ks.my_wallet = wallet
# EOF

View File

@ -9,6 +9,7 @@ from electrum.i18n import _
from electrum.plugin import hook
from electrum.wallet import Standard_Wallet, Multisig_Wallet
from electrum.gui.qt.util import WindowModalDialog, CloseButton, get_parent_main_window, Buttons
from electrum.transaction import Transaction
from .coldcard import ColdcardPlugin, xfp2str
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
@ -18,7 +19,7 @@ from binascii import a2b_hex
from base64 import b64encode, b64decode
from .basic_psbt import BasicPSBT
from .build_psbt import build_psbt
from .build_psbt import build_psbt, combine_psbt
class Plugin(ColdcardPlugin, QtPluginBase):
icon_unpaired = "coldcard_unpaired.png"
@ -84,9 +85,8 @@ class Plugin(ColdcardPlugin, QtPluginBase):
def transaction_dialog(self, dia):
# see gui/qt/transaction_dialog.py
keystore = dia.wallet.get_keystore()
if type(keystore) != self.keystore_class:
# not a Coldcard wallet, hide feature
# if not a Coldcard wallet, hide feature
if not any(type(ks) != self.keystore_class for ks in dia.wallet.get_keystores()):
return
# - add a new button, near "export"
@ -136,7 +136,7 @@ class Plugin(ColdcardPlugin, QtPluginBase):
tools_menu.addAction(_("&Combine PSBT Files"), lambda: self.psbt_combiner(main_window))
def psbt_combiner(self, window):
title = _("Select the signed PSBT files to combine and transmit")
title = _("Select the signed PSBT files to combine")
directory = ''
fnames, __ = QFileDialog.getOpenFileNames(window, title, directory, "PSBT Files (*.psbt)")
@ -154,20 +154,61 @@ class Plugin(ColdcardPlugin, QtPluginBase):
window.show_critical(_("Electrum was unable to open your PSBT file") + "\n" + str(reason), title=_("Unable to read file"))
return
if len(psbts) < 2:
window.show_critical(_("Need 2 or more PSBT to be able to combine them."),
title=_("Unable to combine PSBT files"))
return
# Consistency checks
try:
assert len(psbts) >= 2, _("Need 2 or more PSBT to be able to combine them.")
first = psbts[0]
for p in psbts:
assert (p.txn == first.txn), \
f"All must relate to the same original transaction, check file: {p.filename}"
"All must relate to the same original transaction"
for idx, inp in enumerate(p.inputs):
assert inp.part_sigs, f"No partial signatures found in file: {p.filename}"
assert inp.part_sigs, "No partial signatures found in file"
assert first.inputs[idx].redeem_script == inp.redeem_script, "Mismatched redeem scripts"
assert first.inputs[idx].witness_script == inp.witness_script, "Mismatched witness"
except AssertionError as exc:
window.show_critical(str(exc), title=_("Unable to combine PSBT files"))
window.show_critical(str(exc), title=_("Unable to combine PSBT files, check: ")+p.filename)
return
# Build the transaction, add sigs, and show to user for possible transmission.
tx = Transaction(first.txn.hex())
tx.deserialize(force_full_parse=True)
from electrum.transaction import parse_redeemScript_multisig
# .. add back some data that's been preserved in the PSBT, but isn't part of
# of the unsigned bitcoin txn
tx.is_partial_originally = True
for idx, inp in enumerate(tx.inputs()):
scr = first.inputs[idx].redeem_script
if scr:
M, N, __, pubkeys, __ = parse_redeemScript_multisig(scr)
inp['pubkeys'] = pubkeys
inp['x_pubkeys'] = pubkeys
inp['num_sig'] = M
inp['type'] = 'p2sh' # XXX p2wsh
# bugfix: transaction.pyparse_input puts dict here?
inp['signatures'] = [None] * N
for p in psbts:
try:
combine_psbt(tx, p)
except BaseException as exc:
from PyQt5.QtCore import pyqtRemoveInputHook
pyqtRemoveInputHook()
import pdb; pdb.post_mortem()
window.show_critical(str(exc),
title=_("Unable to combine PSBT file: ") + p.filename)
return
# Display result, might not be complete yet.
window.show_transaction(tx, "PSBT Combined")
class Coldcard_Handler(QtHandlerBase):
setup_signal = pyqtSignal()