Combines signed PSBT files
This commit is contained in:
parent
cc5c0532e5
commit
1bde362ddb
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user