From c04a9a88cad3d5a08368cb505d558a1d1e3d4f60 Mon Sep 17 00:00:00 2001 From: rage-proof <7944736+rage-proof@users.noreply.github.com> Date: Tue, 15 Sep 2020 12:40:26 +0200 Subject: [PATCH 1/7] add payjoin option --- electrum/gui/qt/main_window.py | 68 +++++++++++++++++++++++---- electrum/gui/qt/transaction_dialog.py | 35 +++++++++++--- electrum/invoices.py | 1 + electrum/plugins/hw_wallet/qt.py | 2 +- electrum/tests/test_payjoin.py | 32 +++++++++++++ electrum/transaction.py | 29 ++++++++++++ electrum/util.py | 12 +++-- electrum/wallet.py | 5 ++ 8 files changed, 163 insertions(+), 21 deletions(-) create mode 100644 electrum/tests/test_payjoin.py diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 8ee7704bd85f..531eac5caf69 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -64,7 +64,7 @@ from electrum.invoices import PR_TYPE_ONCHAIN, PR_TYPE_LN, PR_DEFAULT_EXPIRATION_WHEN_CREATING, Invoice from electrum.invoices import PR_PAID, PR_FAILED, pr_expiration_values, LNInvoice, OnchainInvoice from electrum.transaction import (Transaction, PartialTxInput, - PartialTransaction, PartialTxOutput) + PartialTransaction, PartialTxOutput, PayjoinTransaction) from electrum.address_synchronizer import AddTransactionException from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet, sweep_preparations, InternalAddressCorruption) @@ -80,7 +80,7 @@ from .amountedit import AmountEdit, BTCAmountEdit, FreezableLineEdit, FeerateEdit from .qrcodewidget import QRCodeWidget, QRDialog from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit -from .transaction_dialog import show_transaction +from .transaction_dialog import show_transaction, PreviewTxDialog from .fee_slider import FeeSlider, FeeComboBox from .util import (read_QIcon, ColorScheme, text_dialog, icon_path, WaitingDialog, WindowModalDialog, ChoicesLayout, HelpLabel, Buttons, @@ -95,7 +95,6 @@ from .update_checker import UpdateCheck, UpdateCheckThread from .channels_list import ChannelsList from .confirm_tx_dialog import ConfirmTxDialog -from .transaction_dialog import PreviewTxDialog if TYPE_CHECKING: from . import ElectrumGui @@ -1566,6 +1565,7 @@ def do_save_invoice(self): def do_pay(self): invoice = self.read_invoice() + print('main -invoice: ', invoice)# if not invoice: return self.wallet.save_invoice(invoice) @@ -1585,7 +1585,7 @@ def do_pay_invoice(self, invoice: 'Invoice'): self.pay_lightning_invoice(invoice.invoice, amount_msat=invoice.get_amount_msat()) elif invoice.type == PR_TYPE_ONCHAIN: assert isinstance(invoice, OnchainInvoice) - self.pay_onchain_dialog(self.get_coins(), invoice.outputs) + self.pay_onchain_dialog(self.get_coins(), invoice.outputs, payjoin=invoice.bip78_payjoin) else: raise Exception('unknown invoice type') @@ -1605,6 +1605,7 @@ def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]: def pay_onchain_dialog(self, inputs: Sequence[PartialTxInput], outputs: List[PartialTxOutput], *, + payjoin=None, external_keypairs=None) -> None: # trustedcoin requires this if run_hook('abort_send', self): @@ -1632,7 +1633,8 @@ def pay_onchain_dialog(self, inputs: Sequence[PartialTxInput], # shortcut to advanced preview (after "enough funds" check!) if self.config.get('advanced_preview'): self.preview_tx_dialog(make_tx=make_tx, - external_keypairs=external_keypairs) + external_keypairs=external_keypairs, + payjoin=payjoin) return cancelled, is_send, password, tx = d.run() @@ -1646,11 +1648,13 @@ def sign_done(success): external_keypairs=external_keypairs) else: self.preview_tx_dialog(make_tx=make_tx, - external_keypairs=external_keypairs) + external_keypairs=external_keypairs, + payjoin=payjoin) - def preview_tx_dialog(self, *, make_tx, external_keypairs=None): + def preview_tx_dialog(self, *, make_tx, external_keypairs=None, payjoin=None): + print('preview_tx_dialog: ', payjoin)# d = PreviewTxDialog(make_tx=make_tx, external_keypairs=external_keypairs, - window=self) + window=self, payjoin=payjoin) d.show() def broadcast_or_show(self, tx: Transaction): @@ -1726,6 +1730,33 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.on_error) + + def exchange_psbt_http(self, payjoin): + """ """ + import requests, copy + assert payjoin.is_complete() + + print(payjoin.to_json()) + + print(payjoin.serialize_as_base64()) + + + url = 'https://testnet.demo.btcpayserver.org/BTC/pj' + payload = payjoin.serialize_as_base64() + headers = {'content-type': 'text/plain', + 'content-length': str(len(payload)) + } + print(headers) + + try: + r = requests.post(url, data=payload, headers=headers) + except: + pass + + print(payload) + print(r.status_code) + print(r.headers) + print(r.text) def mktx_for_open_channel(self, funding_sat): coins = self.get_coins(nonlocal_only=True) @@ -1869,7 +1900,7 @@ def pay_to_URI(self, URI): if not URI: return try: - out = util.parse_URI(URI, self.on_pr) + out = util.parse_bip21_uri(URI, self.on_pr) except InvalidBitcoinURI as e: self.show_error(_("Error parsing URI") + f":\n{e}") return @@ -1895,7 +1926,21 @@ def pay_to_URI(self, URI): if amount: self.amount_e.setAmount(amount) self.amount_e.textEdited.emit("") - + self._set_payjoin_availability(self.payto_URI) + + def _set_payjoin_availability(self, out): + """ """ + pj = out.get('pj') + pjos = out.get('pjos') + print('pj in main:', pj)# + print('pjos in main:', pjos)# + if pj: + self.pj_available = True + self.pj = pj + self.pjos = pjos + else: + self.pj_available = False + print('pj_available in main:', self.pj_available)# def do_clear(self): self.max_button.setChecked(False) @@ -1903,6 +1948,9 @@ def do_clear(self): self.payto_URI = None self.payto_e.is_pr = False self.set_onchain(False) + self.pj_available = False + self.pj = None + self.pjos = None for e in [self.payto_e, self.message_e, self.amount_e]: e.setText('') e.setFrozen(False) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 2a2ab46cb7ee..758bb85bb2aa 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -92,7 +92,7 @@ def show_transaction(tx: Transaction, *, parent: 'ElectrumWindow', desc=None, pr class BaseTxDialog(QDialog, MessageBoxMixin): - def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None): + def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finalized: bool, external_keypairs=None, payjoin=None): '''Transactions in the wallet will show their description. Pass desc to give a description for txs not yet in the wallet. ''' @@ -105,6 +105,12 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.config = parent.config self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved + if payjoin: + self.pj = payjoin['pj'] + self.pjos = payjoin['pjos'] + self.pj_available = True + else: + self.pj_available = False self.saved = False self.desc = desc self.setMinimumWidth(950) @@ -207,6 +213,7 @@ def set_tx(self, tx: 'Transaction'): # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. self.tx = tx = copy.deepcopy(tx) + print('tx-diaog: set_tx', self.tx)# try: self.tx.deserialize() except BaseException as e: @@ -219,13 +226,18 @@ def set_tx(self, tx: 'Transaction'): def do_broadcast(self): self.main_window.push_top_level_window(self) - try: - self.main_window.broadcast_transaction(self.tx) - finally: - self.main_window.pop_top_level_window(self) + if self.payjoin_cb.isChecked(): + self.main_window.exchange_psbt_http(self._gettx_for_coinjoin()) + else: + try: + self.main_window.broadcast_transaction(self.tx) + finally: + self.main_window.pop_top_level_window(self) self.saved = True self.update() + + def closeEvent(self, event): if (self.prompt_if_unsaved and not self.saved and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))): @@ -641,6 +653,12 @@ def add_tx_stats(self, vbox): self.block_height_label = TxDetailLabel() vbox_right.addWidget(self.block_height_label) + + self.payjoin_cb = QCheckBox(_('PayJoin')) + self.payjoin_cb.setChecked(bool(self.config.get('use_payjoin', True))) + vbox_right.addWidget(self.payjoin_cb) + #visibility + vbox_right.addStretch(1) hbox_stats.addLayout(vbox_right, 50) @@ -653,6 +671,8 @@ def add_tx_stats(self, vbox): # set visibility after parenting can be determined by Qt self.rbf_label.setVisible(self.finalized) self.rbf_cb.setVisible(not self.finalized) + self.payjoin_cb.setVisible(self.pj_available) + print('pj_available in dialog:', self.pj_available)# self.locktime_final_label.setVisible(self.finalized) self.locktime_setter_widget.setVisible(not self.finalized) @@ -687,10 +707,11 @@ def __init__(self, tx: Transaction, *, parent: 'ElectrumWindow', desc, prompt_if class PreviewTxDialog(BaseTxDialog, TxEditor): - def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow'): + def __init__(self, *, make_tx, external_keypairs, window: 'ElectrumWindow', payjoin): TxEditor.__init__(self, window=window, make_tx=make_tx, is_sweep=bool(external_keypairs)) BaseTxDialog.__init__(self, parent=window, desc='', prompt_if_unsaved=False, - finalized=False, external_keypairs=external_keypairs) + finalized=False, external_keypairs=external_keypairs, payjoin=payjoin) + print('pj_available in Preview dialog: ', payjoin)# BlockingWaitingDialog(window, _("Preparing transaction..."), lambda: self.update_tx(fallback_to_zero_fee=True)) self.update() diff --git a/electrum/invoices.py b/electrum/invoices.py index c7879fc7e51b..eb76258fd561 100644 --- a/electrum/invoices.py +++ b/electrum/invoices.py @@ -120,6 +120,7 @@ class OnchainInvoice(Invoice): outputs = attr.ib(kw_only=True, converter=_decode_outputs) # type: List[PartialTxOutput] bip70 = attr.ib(type=str, kw_only=True) # type: Optional[str] requestor = attr.ib(type=str, kw_only=True) # type: Optional[str] + bip78_payjoin = attr.ib(type=Dict, kw_only=True, default=None) # type: Optional[Dict] def get_address(self) -> str: assert len(self.outputs) == 1 diff --git a/electrum/plugins/hw_wallet/qt.py b/electrum/plugins/hw_wallet/qt.py index 56b8b62a2f2e..43f2b70046c6 100644 --- a/electrum/plugins/hw_wallet/qt.py +++ b/electrum/plugins/hw_wallet/qt.py @@ -40,7 +40,7 @@ from electrum.i18n import _ from electrum.logging import Logger -from electrum.util import parse_URI, InvalidBitcoinURI, UserCancelled, UserFacingException +from electrum.util import parse_bip21_uri, InvalidBitcoinURI, UserCancelled, UserFacingException from electrum.plugin import hook, DeviceUnpairableError from .plugin import OutdatedHwFirmwareException, HW_PluginBase, HardwareHandlerBase diff --git a/electrum/tests/test_payjoin.py b/electrum/tests/test_payjoin.py new file mode 100644 index 000000000000..9a7243912f51 --- /dev/null +++ b/electrum/tests/test_payjoin.py @@ -0,0 +1,32 @@ + +from electrum import constants +from electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream, + SerializationError, PSBTInputConsistencyFailure, convert_raw_tx_to_hex + ) + + + + +def main(): + psbt_test01 = 'cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQQWABTHikVyU1WCjVZYB03VJg1fy2mFMCICAxWawBqg1YdUxLTYt9NJ7R7fzws2K09rVRBnI6KFj4UWRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEAFgAURvYaK7pzgo7lhbSl/DeUan2MxRQiAgLKC8FYHmmul/HrXLUcMDCjfuRg/dhEkG8CO26cEC6vfBhIXNZQMQAAgAEAAIAAAACAAQAAAAEAAAAAAA==' + psbt_test02 = 'cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=' + psbt_test03 = 'cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==' + psbt_test04 = 'cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=' + psbt_test05 = 'cHNidP8BAJoCAAAAApmUy3Oa6MdcM+j5cM1w7BIxlGz2D8GN2nK9tsbHmESKAQAAAAD+////BMkXzO/XlgaOAExsQbP9mumAyAslUiwThX1+gqF3Z6MBAAAAAP7///8CrUsAAAAAAAAWABSy6G0APFIK1Mygn/X1It3Sp7sjBpBfAQAAAAAAFgAU2nAe+KqvbhU63rT052pJUslTN3Ml/hsAAAEBH35MAAAAAAAAFgAU5vIbmekoqCDiLHT+jNPVowJ7t3giAgL2ZUi4zj2sIpPU0rQQvA98/9dUOMFyP/y2qhVwbj1zlUcwRAIgDF00uFWZ2c3F1IBiCaKxphKIlSv+ht1Lch8F++B3CAICIA0k1vhC6J6bkHC6jvhl4pYBYQ7TfoClydnLmeTG8Qs8ASIGAvZlSLjOPawik9TStBC8D3z/11Q4wXI//LaqFXBuPXOVGPdYxlpUAACAAQAAgAAAAIABAAAAAQAAAAABAR+QXwEAAAAAABYAFLdBo8LVBUf5Rg1DFUz1o6QjzUv6IgIDgKPoOPCNL7i4H37KFgY6750XHdQps/yQKbpkEO5dCalHMEQCIDdUvZLe4G+sZDPaY02Smner6TrpjWVofMcKCQfeTpHpAiBHAwHEgBGXEYq8O3rDoSFpJgE7B6nWhQGRCixOzxpJHQEiBgOAo+g48I0vuLgffsoWBjrvnRcd1Cmz/JApumQQ7l0JqRj3WMZaVAAAgAEAAIAAAACAAAAAAAgAAAAAIgIDhgLfaU5ScNGBBRX33y/qp7PgZFHQZSGO1qHmIfNtNlAY91jGWlQAAIABAACAAAAAgAEAAAACAAAAACICA5XOZXvPEUSTRt2Nf1qlYDp9Iq1Shm0sxKO7QD39ZWw/GPdYxlpUAACAAQAAgAAAAIAAAAAACQAAAAA=' + + + tx = tx_from_any(psbt_test05) + print('TX') + print(type(tx)) + tx.finalize_psbt() + print('\n',tx.to_json()) + print('\ncomplete? ',tx.is_complete()) + + print('\n',tx._serialize_as_base64()) + + + + + +if __name__ == '__main__': + main() diff --git a/electrum/transaction.py b/electrum/transaction.py index 42f45a2c38f1..7fc517fcdb04 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1876,6 +1876,10 @@ def _serialize_as_base64(self) -> str: raw_bytes = self.serialize_as_bytes() return base64.b64encode(raw_bytes).decode('ascii') + def serialize_as_base64(self, force_psbt = True) -> str: + raw_bytes = self.serialize_as_bytes(force_psbt = force_psbt) + return base64.b64encode(raw_bytes).decode('ascii') + def update_signatures(self, signatures: Sequence[str]): """Add new signatures to a transaction @@ -1984,6 +1988,31 @@ def remove_signatures(self): self.invalidate_ser_cache() +class PayjoinTransaction(PartialTransaction): + + @classmethod + def from_tx(cls, tx: Transaction) -> 'PayjoinTransaction': + res = cls(None) + res._inputs = [PartialTxInput.from_txin(txin, strip_witness = False) for txin in tx.inputs()] + res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()] + res.version = tx.version + res.locktime = tx.locktime + return res + + def serialize_as_base64(self, force_psbt = True) -> str: + raw_bytes = self.serialize_as_bytes(force_psbt = force_psbt) + return base64.b64encode(raw_bytes).decode('ascii') + + def check_for_encrypted_connection(self): + pass + + def create_original_psbt(self): + pass + + def check_payjoin_proposal(self): + pass + + def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: if len(xfp) != 4: raise Exception(f'unexpected xfp length. xfp={xfp}') diff --git a/electrum/util.py b/electrum/util.py index 21333d37634e..f3c0b5bea530 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -822,8 +822,7 @@ def block_explorer_URL(config: 'SimpleConfig', kind: str, item: str) -> Optional class InvalidBitcoinURI(Exception): pass -# TODO rename to parse_bip21_uri or similar -def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: +def parse_bip21_uri(uri: str, on_pr: Callable = None, *, loop=None) -> dict: """Raises InvalidBitcoinURI on malformed URI.""" from . import bitcoin from .bitcoin import COIN @@ -887,7 +886,14 @@ def parse_URI(uri: str, on_pr: Callable = None, *, loop=None) -> dict: out['sig'] = bh2u(bitcoin.base_decode(out['sig'], base=58)) except Exception as e: raise InvalidBitcoinURI(f"failed to parse 'sig' field: {repr(e)}") from e - + if 'pj' in out: + try: + out['pj'] = str(out.get('pj')) + out['pjos'] = int(out.get('pjos',1)) + print(out['pj'])# + print(out['pjos'])# + except Exception as e: + raise InvalidBitcoinURI(f"failed to parse 'pj' field: {repr(e)}") from e r = out.get('r') sig = out.get('sig') name = out.get('name') diff --git a/electrum/wallet.py b/electrum/wallet.py index 07b1d2a1f3a0..07b5ac5324fe 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -693,9 +693,13 @@ def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> amount = sum(x.value for x in outputs) timestamp = None exp = None + bip78_payjoin = dict() if URI: timestamp = URI.get('time') exp = URI.get('exp') + bip78_payjoin['pj'] = URI.get('pj') + bip78_payjoin['pjos'] = URI.get('pjos') + timestamp = timestamp or int(time.time()) exp = exp or 0 invoice = OnchainInvoice( @@ -708,6 +712,7 @@ def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> exp=exp, bip70=None, requestor=None, + bip78_payjoin=bip78_payjoin, ) return invoice From 4ae1950f0ba3a269f13868a9e3dc770717c22a58 Mon Sep 17 00:00:00 2001 From: rage-proof Date: Tue, 15 Sep 2020 13:00:55 +0200 Subject: [PATCH 2/7] testi --- electrum/gui/qt/main_window.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 531eac5caf69..2dcce727c3e5 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1737,8 +1737,21 @@ def exchange_psbt_http(self, payjoin): assert payjoin.is_complete() print(payjoin.to_json()) +<<<<<<< Updated upstream print(payjoin.serialize_as_base64()) +======= + print(payjoin.serialize_as_base64()) + + for txin in payjoin.inputs(): + print(txin) + print(txin.utxo) + print(txin.utxo.outputs()) + print + self.utxo.outputs()[self.prevout.out_idx] + + +>>>>>>> Stashed changes url = 'https://testnet.demo.btcpayserver.org/BTC/pj' @@ -1747,7 +1760,11 @@ def exchange_psbt_http(self, payjoin): 'content-length': str(len(payload)) } print(headers) +<<<<<<< Updated upstream +======= + """ +>>>>>>> Stashed changes try: r = requests.post(url, data=payload, headers=headers) except: @@ -1757,6 +1774,10 @@ def exchange_psbt_http(self, payjoin): print(r.status_code) print(r.headers) print(r.text) +<<<<<<< Updated upstream +======= + """ +>>>>>>> Stashed changes def mktx_for_open_channel(self, funding_sat): coins = self.get_coins(nonlocal_only=True) From dcec305ee40ec982d362cf66f9b97f748ecb53b2 Mon Sep 17 00:00:00 2001 From: rage-proof <47944736+rage-proof@users.noreply.github.com> Date: Tue, 15 Sep 2020 18:25:24 +0200 Subject: [PATCH 3/7] testing witness utxo --- electrum/gui/qt/main_window.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2dcce727c3e5..6234e47a71cc 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1737,34 +1737,28 @@ def exchange_psbt_http(self, payjoin): assert payjoin.is_complete() print(payjoin.to_json()) -<<<<<<< Updated upstream - - print(payjoin.serialize_as_base64()) -======= print(payjoin.serialize_as_base64()) for txin in payjoin.inputs(): print(txin) print(txin.utxo) print(txin.utxo.outputs()) - print - self.utxo.outputs()[self.prevout.out_idx] + print(txin.utxo.outputs()[txin.prevout.out_idx]) + print('\n',txin.to_json()) + print('\n',txin.to_json()) + ->>>>>>> Stashed changes - url = 'https://testnet.demo.btcpayserver.org/BTC/pj' payload = payjoin.serialize_as_base64() headers = {'content-type': 'text/plain', 'content-length': str(len(payload)) } - print(headers) -<<<<<<< Updated upstream + #print(headers) + -======= """ ->>>>>>> Stashed changes try: r = requests.post(url, data=payload, headers=headers) except: @@ -1774,10 +1768,8 @@ def exchange_psbt_http(self, payjoin): print(r.status_code) print(r.headers) print(r.text) -<<<<<<< Updated upstream -======= """ ->>>>>>> Stashed changes + def mktx_for_open_channel(self, funding_sat): coins = self.get_coins(nonlocal_only=True) From c6f7246ae8ebae1155be9090d942335b3dafa02a Mon Sep 17 00:00:00 2001 From: schlamarcel Date: Sat, 19 Sep 2020 21:49:14 +0200 Subject: [PATCH 4/7] fix utxo witness_utxo --- electrum/gui/qt/main_window.py | 33 ++++++++++++++++++++++----------- electrum/tests/test_payjoin.py | 3 +-- electrum/transaction.py | 16 ++++++++++------ 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 6234e47a71cc..e58603b0503e 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1733,7 +1733,7 @@ def broadcast_done(result): def exchange_psbt_http(self, payjoin): """ """ - import requests, copy + import requests assert payjoin.is_complete() print(payjoin.to_json()) @@ -1742,23 +1742,34 @@ def exchange_psbt_http(self, payjoin): for txin in payjoin.inputs(): print(txin) print(txin.utxo) - print(txin.utxo.outputs()) - print(txin.utxo.outputs()[txin.prevout.out_idx]) - print('\n',txin.to_json()) - print('\n',txin.to_json()) - + print('\n',txin.witness_utxo) + print('\n', txin.to_json()) + + if txin.utxo: + print(txin.utxo.outputs()) + print(txin.prevout.out_idx) + print(txin.utxo.outputs()[txin.prevout.out_idx]) + + print() + + + print('\n',txin.utxo) + txin.convert_utxo_to_witness_utxo() + print('\n', txin.witness_utxo) + print('\n', txin.to_json()) + print() + payjoin.convert_all_utxos_to_witness_utxos() + print(payjoin.serialize_as_base64()) + print(payjoin.to_json()) - url = 'https://testnet.demo.btcpayserver.org/BTC/pj' payload = payjoin.serialize_as_base64() headers = {'content-type': 'text/plain', 'content-length': str(len(payload)) } - #print(headers) + print(headers) - - """ try: r = requests.post(url, data=payload, headers=headers) except: @@ -1768,7 +1779,7 @@ def exchange_psbt_http(self, payjoin): print(r.status_code) print(r.headers) print(r.text) - """ + def mktx_for_open_channel(self, funding_sat): diff --git a/electrum/tests/test_payjoin.py b/electrum/tests/test_payjoin.py index 9a7243912f51..18ead67861f1 100644 --- a/electrum/tests/test_payjoin.py +++ b/electrum/tests/test_payjoin.py @@ -12,8 +12,7 @@ def main(): psbt_test02 = 'cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=' psbt_test03 = 'cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==' psbt_test04 = 'cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=' - psbt_test05 = 'cHNidP8BAJoCAAAAApmUy3Oa6MdcM+j5cM1w7BIxlGz2D8GN2nK9tsbHmESKAQAAAAD+////BMkXzO/XlgaOAExsQbP9mumAyAslUiwThX1+gqF3Z6MBAAAAAP7///8CrUsAAAAAAAAWABSy6G0APFIK1Mygn/X1It3Sp7sjBpBfAQAAAAAAFgAU2nAe+KqvbhU63rT052pJUslTN3Ml/hsAAAEBH35MAAAAAAAAFgAU5vIbmekoqCDiLHT+jNPVowJ7t3giAgL2ZUi4zj2sIpPU0rQQvA98/9dUOMFyP/y2qhVwbj1zlUcwRAIgDF00uFWZ2c3F1IBiCaKxphKIlSv+ht1Lch8F++B3CAICIA0k1vhC6J6bkHC6jvhl4pYBYQ7TfoClydnLmeTG8Qs8ASIGAvZlSLjOPawik9TStBC8D3z/11Q4wXI//LaqFXBuPXOVGPdYxlpUAACAAQAAgAAAAIABAAAAAQAAAAABAR+QXwEAAAAAABYAFLdBo8LVBUf5Rg1DFUz1o6QjzUv6IgIDgKPoOPCNL7i4H37KFgY6750XHdQps/yQKbpkEO5dCalHMEQCIDdUvZLe4G+sZDPaY02Smner6TrpjWVofMcKCQfeTpHpAiBHAwHEgBGXEYq8O3rDoSFpJgE7B6nWhQGRCixOzxpJHQEiBgOAo+g48I0vuLgffsoWBjrvnRcd1Cmz/JApumQQ7l0JqRj3WMZaVAAAgAEAAIAAAACAAAAAAAgAAAAAIgIDhgLfaU5ScNGBBRX33y/qp7PgZFHQZSGO1qHmIfNtNlAY91jGWlQAAIABAACAAAAAgAEAAAACAAAAACICA5XOZXvPEUSTRt2Nf1qlYDp9Iq1Shm0sxKO7QD39ZWw/GPdYxlpUAACAAQAAgAAAAIAAAAAACQAAAAA=' - + psbt_test05 = 'cHNidP8BAHECAAAAAVwGnoRUshzXZF7tNBNS1fKIelmiyTKwAsJBh+XIWpw+AAAAAAD+////AnARAQAAAAAAFgAUzvW2236QIPmn3lWUMnuBS1sBlJCTTQAAAAAAABYAFLLobQA8UgrUzKCf9fUi3dKnuyMGaP8bAAABAR+QXwEAAAAAABYAFI1G8mbEyNem7WV7SWRGunhPzzcMIgICfWZ87vV+i25qubq9AqHcaEXNKTMCtYhGdKiYayd4a/JHMEQCIDpRqqM7uIWEXrLcv9f/L/WEHuZarYRgSo2M3eXA0IpzAiAKn1nbiXSVFbdPna7rKENT6PHmvn7HFPE3Wd7lM+NdYgEiBgJ9Znzu9X6Lbmq5ur0CodxoRc0pMwK1iEZ0qJhrJ3hr8hj3WMZaVAAAgAEAAIAAAACAAAAAAAwAAAAAIgIDqpXL2t/kOlPve86KzKVRHKM1ZqhugNaHZ6U3uHBIAtAY91jGWlQAAIABAACAAAAAgAAAAAAPAAAAACICA4YC32lOUnDRgQUV998v6qez4GRR0GUhjtah5iHzbTZQGPdYxlpUAACAAQAAgAAAAIABAAAAAgAAAAA=' tx = tx_from_any(psbt_test05) print('TX') diff --git a/electrum/transaction.py b/electrum/transaction.py index 7fc517fcdb04..5cfaabb9a2ef 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1151,9 +1151,11 @@ def utxo(self): @utxo.setter def utxo(self, value: Optional[Transaction]): - self._utxo = value - self.validate_data() - self.ensure_there_is_only_one_utxo() + if value: + self._utxo = value + self.validate_data() + self._witness_utxo = None + self.ensure_there_is_only_one_utxo() @property def witness_utxo(self): @@ -1161,9 +1163,11 @@ def witness_utxo(self): @witness_utxo.setter def witness_utxo(self, value: Optional[TxOutput]): - self._witness_utxo = value - self.validate_data() - self.ensure_there_is_only_one_utxo() + if value: + self._witness_utxo = value + self.validate_data() + self._utxo = None + self.ensure_there_is_only_one_utxo() def to_json(self): d = super().to_json() From 077e7c7c2512d2982ed5659e30b0949d78811444 Mon Sep 17 00:00:00 2001 From: rage-proof <47944736+rage-proof@users.noreply.github.com> Date: Sat, 10 Oct 2020 01:14:05 +0200 Subject: [PATCH 5/7] add some tests --- electrum/gui/qt/main_window.py | 53 +---------- electrum/gui/qt/transaction_dialog.py | 44 ++++++--- electrum/transaction.py | 130 ++++++++++++++++++++++---- 3 files changed, 141 insertions(+), 86 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index e58603b0503e..363f05b4d637 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1683,8 +1683,10 @@ def on_failure(exc_info): on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success if external_keypairs: # can sign directly + print('\nexternal keys: ',external_keypairs)# task = partial(tx.sign, external_keypairs) else: + print('\nexternal keys2: ', external_keypairs) # task = partial(self.wallet.sign_transaction, tx, password) msg = _('Signing transaction...') WaitingDialog(self, msg, task, on_success, on_failure) @@ -1731,57 +1733,6 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.on_error) - def exchange_psbt_http(self, payjoin): - """ """ - import requests - assert payjoin.is_complete() - - print(payjoin.to_json()) - print(payjoin.serialize_as_base64()) - - for txin in payjoin.inputs(): - print(txin) - print(txin.utxo) - print('\n',txin.witness_utxo) - print('\n', txin.to_json()) - - if txin.utxo: - print(txin.utxo.outputs()) - print(txin.prevout.out_idx) - print(txin.utxo.outputs()[txin.prevout.out_idx]) - - print() - - - print('\n',txin.utxo) - txin.convert_utxo_to_witness_utxo() - print('\n', txin.witness_utxo) - print('\n', txin.to_json()) - print() - payjoin.convert_all_utxos_to_witness_utxos() - print(payjoin.serialize_as_base64()) - print(payjoin.to_json()) - - - url = 'https://testnet.demo.btcpayserver.org/BTC/pj' - payload = payjoin.serialize_as_base64() - headers = {'content-type': 'text/plain', - 'content-length': str(len(payload)) - } - print(headers) - - try: - r = requests.post(url, data=payload, headers=headers) - except: - pass - - print(payload) - print(r.status_code) - print(r.headers) - print(r.text) - - - def mktx_for_open_channel(self, funding_sat): coins = self.get_coins(nonlocal_only=True) make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel(coins=coins, diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 758bb85bb2aa..a1d80ea878a7 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -45,7 +45,7 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config -from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput +from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, PayjoinTransaction from electrum.logging import get_logger from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, @@ -105,12 +105,10 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.config = parent.config self.wallet = parent.wallet self.prompt_if_unsaved = prompt_if_unsaved - if payjoin: - self.pj = payjoin['pj'] - self.pjos = payjoin['pjos'] - self.pj_available = True - else: - self.pj_available = False + + self.payjoin = PayjoinTransaction(payjoin) + print(self.payjoin)# + self.saved = False self.desc = desc self.setMinimumWidth(950) @@ -224,15 +222,31 @@ def set_tx(self, tx: 'Transaction'): # note: this might fetch prev txs over the network. tx.add_info_from_wallet(self.wallet) + + def do_broadcast(self): self.main_window.push_top_level_window(self) + if self.payjoin_cb.isChecked(): - self.main_window.exchange_psbt_http(self._gettx_for_coinjoin()) - else: - try: - self.main_window.broadcast_transaction(self.tx) - finally: - self.main_window.pop_top_level_window(self) + self.payjoin.set_tx(self.tx) + tx = self.payjoin.do_payjoin() + if tx is not None: + #self.set_tx(tx) + self.tx = copy.deepcopy(tx) + print('broadcast1: ', self.tx.to_json())# + print('broadcast1: ', self.tx.serialize_as_base64())# + self.sign() + print('external keyspairs', self.external_keypairs) + print('broadcast2: ', self.tx.to_json())# + print('broadcast2: ', self.tx.serialize_as_base64())# + print('broadcast2: ', self.tx._serialize_as_base64()) # + print('broadcast2: ', self.tx.is_complete()) # + """ + try: + self.main_window.broadcast_transaction(self.tx) + finally: + self.main_window.pop_top_level_window(self) + """ self.saved = True self.update() @@ -671,8 +685,8 @@ def add_tx_stats(self, vbox): # set visibility after parenting can be determined by Qt self.rbf_label.setVisible(self.finalized) self.rbf_cb.setVisible(not self.finalized) - self.payjoin_cb.setVisible(self.pj_available) - print('pj_available in dialog:', self.pj_available)# + self.payjoin_cb.setVisible(self.payjoin.is_available()) + print('pj_available in dialog:', self.payjoin.is_available())# self.locktime_final_label.setVisible(self.finalized) self.locktime_setter_widget.setVisible(not self.finalized) diff --git a/electrum/transaction.py b/electrum/transaction.py index 5cfaabb9a2ef..b0f3832e0ff7 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -38,6 +38,8 @@ from enum import IntEnum import itertools import binascii +import requests +import copy from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node @@ -1345,12 +1347,14 @@ def scriptpubkey(self) -> Optional[bytes]: return None def is_complete(self) -> bool: - if self.script_sig is not None and self.witness is not None: + if self.script_sig is not None or self.witness is not None: return True if self.is_coinbase_input(): return True if self.script_sig is not None and not Transaction.is_segwit_input(self): return True + if self.witness is not None and Transaction.is_segwit_input(self): + return True signatures = list(self.part_sigs.values()) s = len(signatures) # note: The 'script_type' field is currently only set by the wallet, @@ -1374,7 +1378,7 @@ def clear_fields_when_finalized(): self.redeem_script = None self.witness_script = None - if self.script_sig is not None and self.witness is not None: + if self.script_sig is not None or self.witness is not None: clear_fields_when_finalized() return # already finalized if self.is_complete(): @@ -1992,29 +1996,115 @@ def remove_signatures(self): self.invalidate_ser_cache() -class PayjoinTransaction(PartialTransaction): +class PayjoinTransaction(): - @classmethod - def from_tx(cls, tx: Transaction) -> 'PayjoinTransaction': - res = cls(None) - res._inputs = [PartialTxInput.from_txin(txin, strip_witness = False) for txin in tx.inputs()] - res._outputs = [PartialTxOutput.from_txout(txout) for txout in tx.outputs()] - res.version = tx.version - res.locktime = tx.locktime - return res + def __init__(self, payjoin_link=None): + self._version = 1 + self.pj = payjoin_link.get('pj') if payjoin_link else None + self.pjos = payjoin_link.get('pjos') if payjoin_link else None - def serialize_as_base64(self, force_psbt = True) -> str: - raw_bytes = self.serialize_as_bytes(force_psbt = force_psbt) - return base64.b64encode(raw_bytes).decode('ascii') + self.payjoin_original = None + self.pj_proposal_received = False - def check_for_encrypted_connection(self): - pass - def create_original_psbt(self): - pass + def is_available(self): + return self.pj is not None - def check_payjoin_proposal(self): - pass + def set_tx(self, tx: PartialTransaction) -> None: + if self.is_available(): + self.tx = copy.deepcopy(tx) + + def prepare_original_psbt(self): + assert not self.pj_proposal_received + assert self.tx.is_complete() + self.payjoin_original = copy.deepcopy(self.tx) + self.payjoin_original.prepare_for_export_for_coinjoin() + self.payjoin_original.convert_all_utxos_to_witness_utxos() + + def do_payjoin(self): + self.prepare_original_psbt() + print('\noriginal psbt',self.payjoin_original.to_json())# + for i,txin in enumerate(self.payjoin_original.inputs()):# + print('\n txinputs:',i,txin.to_json()) # + + + if not self.exchange_payjoin_original(): + return None + self.pj_proposal = PartialTransaction.from_raw_psbt(self.payjoin_proposal_b64) + self.pj_proposal_received = True + if not self.validate_payjoin_proposal(): + return None + return self.pj_proposal + + + + def exchange_payjoin_original(self, url=None): + """ """ + url = self.pj + payload = self.payjoin_original.serialize_as_base64() + headers = {'content-type': 'text/plain', + 'content-length': str(len(payload)) + } + print('header ',headers)# + try: + r = requests.post(url, data=payload, headers=headers) + print(r.status_code)# + print(r.text)# + if r.status_code==200: + self.payjoin_proposal_b64 = r.text + print(self.payjoin_proposal_b64)# + else: + _logger.debug(f"payjoin is flawed {r.text}") + return False + except Exception as e: + print(repr(e))# + return False + return True + + @staticmethod + def validate_inputs_and_outputs(pj_original: PartialTransaction, pj_proposal: PartialTransaction) -> Optional[bool]: + for txin in pj_original.inputs(): + # check if order is important + if not txin.prevout.to_str() in [x.prevout.to_str() for x in pj_proposal.inputs()]: + # list(payjoin_proposal.inputs()).prevout.to_str(): + raise Exception(f"Inputs from the original payjoin missing in payjoin proposal.") + for txin in pj_proposal.inputs(): + if not txin.is_complete() and not txin.prevout.to_str() in [x.prevout.to_str() for x in pj_original.inputs()]: + raise Exception(f"Newly added inputs are not signed.") + + for txout in pj_original.outputs(): + # check if order is important + if txout.is_mine and not txout.scriptpubkey.hex() in [x.scriptpubkey.hex() for x in pj_proposal.outputs()]: + raise Exception(f"Sender outputs from the original payjoin missing in payjoin proposal.") + for txout in pj_proposal.outputs(): + if not txout.scriptpubkey.hex() in [x.scriptpubkey.hex() for x in pj_original.outputs()]: + raise Exception(f"Newly added outputs.") + return True + + + def validate_payjoin_proposal(self): + + if not self.validate_inputs_and_outputs(self.payjoin_original, self.pj_proposal): + return False + """ + if self.payjoin_original.get_fee() > self.payjoin_proposal.get_fee(): + return False + """ + for i, txin in enumerate(self.tx.inputs()): + if self.pj_proposal._inputs[i].prevout.to_str() != txin.prevout.to_str(): + raise Exception(f"Inputs from the original payjoin missing in payjoin proposal2.") + else: + print('\n sig \n',txin.to_json()) + #procedure for parttxin + txin.part_sigs = {} + txin.script_sig = None + txin.witness = None + self.pj_proposal._inputs[i] = txin + print('\n sig2 \n', txin.to_json()) + + + self.pj_proposal.invalidate_ser_cache() + return True def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: From e1834aab2f414a8b66a355d2298c1a51795461e3 Mon Sep 17 00:00:00 2001 From: rage-proof <47944736+rage-proof@users.noreply.github.com> Date: Tue, 3 Nov 2020 23:38:02 +0100 Subject: [PATCH 6/7] add validation tests --- electrum/gui/qt/transaction_dialog.py | 62 +++--- electrum/tests/test_payjoin.py | 31 --- electrum/transaction.py | 259 +++++++++++++++++--------- 3 files changed, 210 insertions(+), 142 deletions(-) delete mode 100644 electrum/tests/test_payjoin.py diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index a1d80ea878a7..d58c0332e602 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -45,7 +45,8 @@ from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config -from electrum.transaction import SerializationError, Transaction, PartialTransaction, PartialTxInput, PayjoinTransaction +from electrum.transaction import (SerializationError, Transaction, PartialTransaction, PartialTxInput, PayjoinTransaction, + PayJoinProposalValidationException, PayJoinExchangeException) from electrum.logging import get_logger from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, @@ -107,7 +108,7 @@ def __init__(self, *, parent: 'ElectrumWindow', desc, prompt_if_unsaved, finaliz self.prompt_if_unsaved = prompt_if_unsaved self.payjoin = PayjoinTransaction(payjoin) - print(self.payjoin)# + self.payjoin_finished = False self.saved = False self.desc = desc @@ -211,7 +212,6 @@ def set_tx(self, tx: 'Transaction'): # e.g. the FX plugin. If this happens during or after a long # sign operation the signatures are lost. self.tx = tx = copy.deepcopy(tx) - print('tx-diaog: set_tx', self.tx)# try: self.tx.deserialize() except BaseException as e: @@ -222,36 +222,51 @@ def set_tx(self, tx: 'Transaction'): # note: this might fetch prev txs over the network. tx.add_info_from_wallet(self.wallet) + def do_payjoin(self) -> None: + def sign_done(success): + self.payjoin_finished = True + if self.tx.get_fee_rate() < self.payjoin._minfeerate: + self.set_tx(original_tx) + _logger.warning("The receiver used a too low fee rate.") + self.show_error( + _("Error creating a payjoin") + ":\n" + + _("The receiver used a too low fee rate") + "\n" + + _("Sending the original transaction")) + self.update() + self.main_window.pop_top_level_window(self) + self.do_broadcast() - - def do_broadcast(self): self.main_window.push_top_level_window(self) - - if self.payjoin_cb.isChecked(): + original_tx = copy.deepcopy(self.tx) + _logger.info(f"Starting Payjoin Session") + try: self.payjoin.set_tx(self.tx) - tx = self.payjoin.do_payjoin() - if tx is not None: - #self.set_tx(tx) - self.tx = copy.deepcopy(tx) - print('broadcast1: ', self.tx.to_json())# - print('broadcast1: ', self.tx.serialize_as_base64())# - self.sign() - print('external keyspairs', self.external_keypairs) - print('broadcast2: ', self.tx.to_json())# - print('broadcast2: ', self.tx.serialize_as_base64())# - print('broadcast2: ', self.tx._serialize_as_base64()) # - print('broadcast2: ', self.tx.is_complete()) # - """ + self.payjoin.do_payjoin() + self.payjoin.payjoin_proposal.add_info_from_wallet(self.wallet) + self.payjoin.validate_payjoin_proposal() + except (PayJoinProposalValidationException, PayJoinExchangeException) as e: + _logger.warning(repr(e)) + self.payjoin_cb.setChecked(False) + self.show_error(_("Error creating a payjoin") + ":\n" + str(e) + "\n" + + _("Sending the original transaction")) + self.do_broadcast() + return + tx = self.payjoin.payjoin_proposal + self.set_tx(tx) + self.main_window.sign_tx(self.tx, callback=sign_done, external_keypairs=self.external_keypairs) + + def do_broadcast(self) -> None: + if self.payjoin_cb.isChecked() and self.payjoin.is_available() and not self.payjoin_finished: + self.do_payjoin() + return + self.main_window.push_top_level_window(self) try: self.main_window.broadcast_transaction(self.tx) finally: self.main_window.pop_top_level_window(self) - """ self.saved = True self.update() - - def closeEvent(self, event): if (self.prompt_if_unsaved and not self.saved and not self.question(_('This transaction is not saved. Close anyway?'), title=_("Warning"))): @@ -686,7 +701,6 @@ def add_tx_stats(self, vbox): self.rbf_label.setVisible(self.finalized) self.rbf_cb.setVisible(not self.finalized) self.payjoin_cb.setVisible(self.payjoin.is_available()) - print('pj_available in dialog:', self.payjoin.is_available())# self.locktime_final_label.setVisible(self.finalized) self.locktime_setter_widget.setVisible(not self.finalized) diff --git a/electrum/tests/test_payjoin.py b/electrum/tests/test_payjoin.py deleted file mode 100644 index 18ead67861f1..000000000000 --- a/electrum/tests/test_payjoin.py +++ /dev/null @@ -1,31 +0,0 @@ - -from electrum import constants -from electrum.transaction import (tx_from_any, PartialTransaction, BadHeaderMagic, UnexpectedEndOfStream, - SerializationError, PSBTInputConsistencyFailure, convert_raw_tx_to_hex - ) - - - - -def main(): - psbt_test01 = 'cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQQWABTHikVyU1WCjVZYB03VJg1fy2mFMCICAxWawBqg1YdUxLTYt9NJ7R7fzws2K09rVRBnI6KFj4UWRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEAFgAURvYaK7pzgo7lhbSl/DeUan2MxRQiAgLKC8FYHmmul/HrXLUcMDCjfuRg/dhEkG8CO26cEC6vfBhIXNZQMQAAgAEAAIAAAACAAQAAAAEAAAAAAA==' - psbt_test02 = 'cHNidP8BAHMCAAAAAY8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////AtyVuAUAAAAAF6kUHehJ8GnSdBUOOv6ujXLrWmsJRDCHgIQeAAAAAAAXqRR3QJbbz0hnQ8IvQ0fptGn+votneofTAAAAAAEBIKgb1wUAAAAAF6kU3k4ekGHKWRNbA1rV5tR5kEVDVNCHAQcXFgAUx4pFclNVgo1WWAdN1SYNX8tphTABCGsCRzBEAiB8Q+A6dep+Rz92vhy26lT0AjZn4PRLi8Bf9qoB/CMk0wIgP/Rj2PWZ3gEjUkTlhDRNAQ0gXwTO7t9n+V14pZ6oljUBIQMVmsAaoNWHVMS02LfTSe0e388LNitPa1UQZyOihY+FFgABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUAAA=' - psbt_test03 = 'cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQAAAA==' - psbt_test04 = 'cHNidP8BAJwCAAAAAo8nutGgJdyYGXWiBEb45Hoe9lWGbkxh/6bNiOJdCDuDAAAAAAD+////jye60aAl3JgZdaIERvjkeh72VYZuTGH/ps2I4l0IO4MBAAAAAP7///8CJpW4BQAAAAAXqRQd6EnwadJ0FQ46/q6NcutaawlEMIcACT0AAAAAABepFHdAltvPSGdDwi9DR+m0af6+i2d6h9MAAAAAAQEgqBvXBQAAAAAXqRTeTh6QYcpZE1sDWtXm1HmQRUNU0IcBBBYAFMeKRXJTVYKNVlgHTdUmDV/LaYUwIgYDFZrAGqDVh1TEtNi300ntHt/PCzYrT2tVEGcjooWPhRYYSFzWUDEAAIABAACAAAAAgAEAAAAAAAAAAAEBIICEHgAAAAAAF6kUyPLL+cphRyyI5GTUazV0hF2R2NWHAQcXFgAUX4BmVeWSTJIEwtUb5TlPS/ntohABCGsCRzBEAiBnu3tA3yWlT0WBClsXXS9j69Bt+waCs9JcjWtNjtv7VgIge2VYAaBeLPDB6HGFlpqOENXMldsJezF9Gs5amvDQRDQBIQJl1jz1tBt8hNx2owTm+4Du4isx0pmdKNMNIjjaMHFfrQABABYAFEb2Giu6c4KO5YW0pfw3lGp9jMUUIgICygvBWB5prpfx61y1HDAwo37kYP3YRJBvAjtunBAur3wYSFzWUDEAAIABAACAAAAAgAEAAAABAAAAAAA=' - psbt_test05 = 'cHNidP8BAHECAAAAAVwGnoRUshzXZF7tNBNS1fKIelmiyTKwAsJBh+XIWpw+AAAAAAD+////AnARAQAAAAAAFgAUzvW2236QIPmn3lWUMnuBS1sBlJCTTQAAAAAAABYAFLLobQA8UgrUzKCf9fUi3dKnuyMGaP8bAAABAR+QXwEAAAAAABYAFI1G8mbEyNem7WV7SWRGunhPzzcMIgICfWZ87vV+i25qubq9AqHcaEXNKTMCtYhGdKiYayd4a/JHMEQCIDpRqqM7uIWEXrLcv9f/L/WEHuZarYRgSo2M3eXA0IpzAiAKn1nbiXSVFbdPna7rKENT6PHmvn7HFPE3Wd7lM+NdYgEiBgJ9Znzu9X6Lbmq5ur0CodxoRc0pMwK1iEZ0qJhrJ3hr8hj3WMZaVAAAgAEAAIAAAACAAAAAAAwAAAAAIgIDqpXL2t/kOlPve86KzKVRHKM1ZqhugNaHZ6U3uHBIAtAY91jGWlQAAIABAACAAAAAgAAAAAAPAAAAACICA4YC32lOUnDRgQUV998v6qez4GRR0GUhjtah5iHzbTZQGPdYxlpUAACAAQAAgAAAAIABAAAAAgAAAAA=' - - tx = tx_from_any(psbt_test05) - print('TX') - print(type(tx)) - tx.finalize_psbt() - print('\n',tx.to_json()) - print('\ncomplete? ',tx.is_complete()) - - print('\n',tx._serialize_as_base64()) - - - - - -if __name__ == '__main__': - main() diff --git a/electrum/transaction.py b/electrum/transaction.py index b0f3832e0ff7..803e135e08af 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -41,6 +41,8 @@ import requests import copy +#from .lnutil import make_htlc_tx_output + from . import ecc, bitcoin, constants, segwit_addr, bip32 from .bip32 import BIP32Node from .util import profiler, to_bytes, bh2u, bfh, chunks, is_hex_str @@ -1461,6 +1463,11 @@ def already_has_some_signatures(self) -> bool: or self.script_sig is not None or self.witness is not None) + def remove_signature(self): + self.part_sigs = {} + self.script_sig = None + self.witness = None + class PartialTxOutput(TxOutput, PSBTSection): def __init__(self, *args, **kwargs): @@ -1996,115 +2003,193 @@ def remove_signatures(self): self.invalidate_ser_cache() +class PayJoinExchangeException(Exception): + pass + +class PayJoinProposalValidationException(Exception): + pass + class PayjoinTransaction(): + VERSION = 1 + VSIZE_SENDER_TYPE ={'p2wpkh':68, + 'p2pkh':148, + 'p2wpkh - p2sh':91 + } def __init__(self, payjoin_link=None): - self._version = 1 self.pj = payjoin_link.get('pj') if payjoin_link else None self.pjos = payjoin_link.get('pjos') if payjoin_link else None - self.payjoin_original = None - self.pj_proposal_received = False + self._additionalfeeoutputindex = None + self._maxadditionalfeecontribution = None + self._minfeerate = None + self._disableoutputsubstitution = None + self.payjoin_original = None + self.payjoin_proposal = None - def is_available(self): - return self.pj is not None + def is_available(self) -> bool: + return self.pj is not None and isinstance(self.pj, str) def set_tx(self, tx: PartialTransaction) -> None: - if self.is_available(): - self.tx = copy.deepcopy(tx) - - def prepare_original_psbt(self): - assert not self.pj_proposal_received - assert self.tx.is_complete() - self.payjoin_original = copy.deepcopy(self.tx) + self.payjoin_original = copy.deepcopy(tx) + self.payjoin_proposal_received = False + self.define_sender_params() + self.prepare_payjoin_original() + + def define_sender_params(self): + def examine_change_output(): + for i, txout in enumerate(self.payjoin_original.outputs()): + if (txout.is_mine and txout.is_change): + return i + def examine_input_vsize(tx): + txin = tx._inputs[0] + return self.VSIZE_SENDER_TYPE.get(txin.script_type) + # set min fee rate to an int that is rounded down + self._minfeerate = int(self.payjoin_original.get_fee_rate()) + self._disableoutputsubstitution = 'true' if self.pjos == 0 else 'false' + self._additionalfeeoutputindex = examine_change_output() + # use a fee rate that is rounded to a full int + self.original_psbt_fee_rate = round(self.payjoin_original.get_fee_rate()) + self.vsize_input_type = examine_input_vsize(self.payjoin_original) + self._maxadditionalfeecontribution = int(self.original_psbt_fee_rate * self.vsize_input_type) + + def prepare_payjoin_original(self): + assert not self.payjoin_proposal_received + assert self.payjoin_original.is_complete() self.payjoin_original.prepare_for_export_for_coinjoin() - self.payjoin_original.convert_all_utxos_to_witness_utxos() - - def do_payjoin(self): - self.prepare_original_psbt() - print('\noriginal psbt',self.payjoin_original.to_json())# - for i,txin in enumerate(self.payjoin_original.inputs()):# - print('\n txinputs:',i,txin.to_json()) # - - - if not self.exchange_payjoin_original(): - return None - self.pj_proposal = PartialTransaction.from_raw_psbt(self.payjoin_proposal_b64) - self.pj_proposal_received = True - if not self.validate_payjoin_proposal(): - return None - return self.pj_proposal + if self.payjoin_original.is_segwit(): + self.payjoin_original.convert_all_utxos_to_witness_utxos() + def do_payjoin(self): + self.exchange_payjoin_original() + self.payjoin_proposal = PartialTransaction.from_raw_psbt(self.payjoin_proposal_b64) + self.payjoin_proposal_received = True + self.payjoin_proposal.invalidate_ser_cache() - def exchange_payjoin_original(self, url=None): + """ + @staticmethod + def input_vsize(tx: PartialTransaction) -> int: + txin = tx._inputs[0] + is_segwit = tx.is_segwit_input(txin) + input_weight = tx.estimated_input_weight(txin, is_segwit) + return tx.virtual_size_from_weight(input_weight) + """ + + def exchange_payjoin_original(self) -> None: """ """ url = self.pj payload = self.payjoin_original.serialize_as_base64() headers = {'content-type': 'text/plain', 'content-length': str(len(payload)) } - print('header ',headers)# - try: - r = requests.post(url, data=payload, headers=headers) - print(r.status_code)# - print(r.text)# - if r.status_code==200: - self.payjoin_proposal_b64 = r.text - print(self.payjoin_proposal_b64)# - else: - _logger.debug(f"payjoin is flawed {r.text}") - return False + query_string = '?v=' + str(self.VERSION) + query_string += '&additionalfeeoutputindex=' + str(self._additionalfeeoutputindex) + query_string += '&maxadditionalfeecontribution=' + str(self._maxadditionalfeecontribution) + query_string += '&minfeerate=' + str(self._minfeerate) + query_string += '&disableoutputsubstitution=' + self._disableoutputsubstitution + url += query_string + _logger.warning(f"url: {url}")# + session = requests.Session() + if self.pj.endswith('.onion'): + session.proxies = {'http': 'socks5h://localhost:9050', + 'https': 'socks5h://localhost:9050', + } + try: + r = session.post(url, data=payload, headers=headers) + assert r.status_code==200 + self.payjoin_proposal_b64 = r.text + except AssertionError: + raise PayJoinExchangeException(f"Exchange of payjoin failed. {r.status_code}: {r.text}") except Exception as e: - print(repr(e))# - return False - return True - - @staticmethod - def validate_inputs_and_outputs(pj_original: PartialTransaction, pj_proposal: PartialTransaction) -> Optional[bool]: - for txin in pj_original.inputs(): - # check if order is important - if not txin.prevout.to_str() in [x.prevout.to_str() for x in pj_proposal.inputs()]: - # list(payjoin_proposal.inputs()).prevout.to_str(): - raise Exception(f"Inputs from the original payjoin missing in payjoin proposal.") - for txin in pj_proposal.inputs(): - if not txin.is_complete() and not txin.prevout.to_str() in [x.prevout.to_str() for x in pj_original.inputs()]: - raise Exception(f"Newly added inputs are not signed.") - - for txout in pj_original.outputs(): - # check if order is important - if txout.is_mine and not txout.scriptpubkey.hex() in [x.scriptpubkey.hex() for x in pj_proposal.outputs()]: - raise Exception(f"Sender outputs from the original payjoin missing in payjoin proposal.") - for txout in pj_proposal.outputs(): - if not txout.scriptpubkey.hex() in [x.scriptpubkey.hex() for x in pj_original.outputs()]: - raise Exception(f"Newly added outputs.") - return True - - - def validate_payjoin_proposal(self): - - if not self.validate_inputs_and_outputs(self.payjoin_original, self.pj_proposal): - return False - """ + raise PayJoinExchangeException(f"Exchange of payjoin failed. {repr(e)}") + + + def validate_payjoin_proposal(self) -> None: + + # check transaction version + if self.payjoin_original.version != self.payjoin_proposal.version: + raise PayJoinProposalValidationException(f"The transactin version in payjoin proposal was modified.") + # check transaction locktime + if self.payjoin_original.locktime != self.payjoin_proposal.locktime: + raise PayJoinProposalValidationException(f"The transactin locktime in payjoin proposal was modified.") + + # check whether all inputs from the original psbt are present in the proposal + for txin in self.payjoin_original.inputs(): + sender_input_index = [i for i, x in enumerate(self.payjoin_proposal.inputs()) if txin.prevout.to_str() == x.prevout.to_str()] + if len(sender_input_index) != 1: + raise PayJoinProposalValidationException(f"Inputs from the original payjoin missing in payjoin proposal.") + sender_input = self.payjoin_proposal._inputs[sender_input_index[0]] + if sender_input.is_complete(): + raise PayJoinProposalValidationException(f"Inputs from the original payjoin is already finalized.") + if txin.nsequence != sender_input.nsequence: + raise PayJoinProposalValidationException(f"Inputs nsequence from the original payjoin was modified.") + + sequences = set() + # check wheter the new Inputs are finalized and utxo data is filled + for txin in self.payjoin_proposal.inputs(): + if not txin.prevout.to_str() in [x.prevout.to_str() for x in self.payjoin_original.inputs()]: + if not txin.is_complete(): + raise PayJoinProposalValidationException(f"Newly added input is not finalized.") + sequences.add(txin.nsequence) + + # check that all inputs use the same sequence number + if len(sequences) != 1: + raise PayJoinProposalValidationException(f"Payjoin roposal introduced different sequence numbers.") + + #TODO: check the order of inputs and script Type + + # check the absolute fee was not decreased if self.payjoin_original.get_fee() > self.payjoin_proposal.get_fee(): - return False - """ - for i, txin in enumerate(self.tx.inputs()): - if self.pj_proposal._inputs[i].prevout.to_str() != txin.prevout.to_str(): - raise Exception(f"Inputs from the original payjoin missing in payjoin proposal2.") - else: - print('\n sig \n',txin.to_json()) - #procedure for parttxin - txin.part_sigs = {} - txin.script_sig = None - txin.witness = None - self.pj_proposal._inputs[i] = txin - print('\n sig2 \n', txin.to_json()) - - - self.pj_proposal.invalidate_ser_cache() - return True + raise PayJoinProposalValidationException(f"The total fee was decreased in the payjoin proposal.") + + # check change output + change_output_o = self.payjoin_original._outputs[self._additionalfeeoutputindex] + change_output_p = None + for txout in self.payjoin_proposal.outputs(): + if change_output_o.scriptpubkey.hex() == txout.scriptpubkey.hex(): + change_output_p = txout + break + # check that the change output still exists + if not change_output_p: + raise PayJoinProposalValidationException(f"Change outputs is missing in the payjoin proposal.") + # check that not too much fees were subtracted + add_fee = change_output_o.value - change_output_p.value + if add_fee > self._maxadditionalfeecontribution: + raise PayJoinProposalValidationException(f"More fee's were added then defined in the payjoin.") + print('add fee change', add_fee)# + print('add fee transaction', (self.payjoin_proposal.get_fee() - self.payjoin_original.get_fee())) # + # check + if add_fee > (self.payjoin_proposal.get_fee() - self.payjoin_original.get_fee()): # should be equal + raise PayJoinProposalValidationException(f"Too much fees were subtracted.") + # check the case that no additional input was added but the fee raised + if add_fee > (self.original_psbt_fee_rate * self.vsize_input_type * + (len(self.payjoin_proposal.inputs()) - len(self.payjoin_original.inputs()))): + raise PayJoinProposalValidationException(f"Fee's were added but no receiver Inputs.") + + # check additional sender outputs are present in the proposal + for txout in self.payjoin_original.outputs(): + if txout.is_change: + continue + if txout.is_mine: + sender_o = [x for x in self.payjoin_proposal.outputs() if txout.scriptpubkey.hex() == x.scriptpubkey.hex()] + if len(sender_o) != 1: + raise PayJoinProposalValidationException(f"Sender outputs from the original payjoin missing in payjoin proposal.") + # check that the amount from the sender that is not fee output was not decreased + if sender_o[0].value < txout.value: + raise PayJoinProposalValidationException(f"The ouput amount for us was decreased in the payjoin.") + + # if output substitution if forbidden, check the outputs and values for the receiver + if self.pjos == 0: + for txout in self.payjoin_proposal.outputs(): + if txout.is_mine or txout.is_change: + continue + receiver_o = [x for x in self.payjoin_original.outputs() if txout.scriptpubkey.hex() == x.scriptpubkey.hex()] + if len(receiver_o) != 1: + raise PayJoinProposalValidationException(f"Receiver modified the payment outputs, but it was forbidden.") + if receiver_o[0].value < txout.value: + raise PayJoinProposalValidationException(f"The amount of the payment ouput was decreased.") def pack_bip32_root_fingerprint_and_int_path(xfp: bytes, path: Sequence[int]) -> bytes: From 9827a8c02da6a752a345d39473daffca7c7e99ce Mon Sep 17 00:00:00 2001 From: rage-proof <47944736+rage-proof@users.noreply.github.com> Date: Thu, 5 Nov 2020 22:01:47 +0100 Subject: [PATCH 7/7] add validation tests --- electrum/gui/qt/main_window.py | 41 +++++++++++++++++++++++++-- electrum/gui/qt/transaction_dialog.py | 3 ++ electrum/transaction.py | 33 +++++++++++++++++++-- 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 363f05b4d637..c99ffae2f340 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1683,10 +1683,8 @@ def on_failure(exc_info): on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success if external_keypairs: # can sign directly - print('\nexternal keys: ',external_keypairs)# task = partial(tx.sign, external_keypairs) else: - print('\nexternal keys2: ', external_keypairs) # task = partial(self.wallet.sign_transaction, tx, password) msg = _('Signing transaction...') WaitingDialog(self, msg, task, on_success, on_failure) @@ -1733,6 +1731,45 @@ def broadcast_done(result): WaitingDialog(self, _('Broadcasting transaction...'), broadcast_thread, broadcast_done, self.on_error) +<<<<<<< Updated upstream +======= + def exchange_psbt_http(self, payjoin): + """ """ + import requests, copy + assert payjoin.is_complete() + + print(payjoin.to_json()) + print(payjoin.serialize_as_base64()) + + for txin in payjoin.inputs(): + print(txin) + print(txin.utxo) + print(txin.utxo.outputs()) + print + self.utxo.outputs()[self.prevout.out_idx] + + + + + url = 'https://testnet.demo.btcpayserver.org/BTC/pj' + payload = payjoin.serialize_as_base64() + headers = {'content-type': 'text/plain', + 'content-length': str(len(payload)) + } + print(headers) + """ + try: + r = requests.post(url, data=payload, headers=headers) + except: + pass + + print(payload) + print(r.status_code) + print(r.headers) + print(r.text) + """ + +>>>>>>> Stashed changes def mktx_for_open_channel(self, funding_sat): coins = self.get_coins(nonlocal_only=True) make_tx = lambda fee_est: self.wallet.lnworker.mktx_for_open_channel(coins=coins, diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index d58c0332e602..8d99f73b72be 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -241,8 +241,11 @@ def sign_done(success): _logger.info(f"Starting Payjoin Session") try: self.payjoin.set_tx(self.tx) + print('tx1: ',self.tx.to_json())# self.payjoin.do_payjoin() + print('tx2: ', self.payjoin.payjoin_proposal.to_json()) # self.payjoin.payjoin_proposal.add_info_from_wallet(self.wallet) + print('tx3: ', self.payjoin.payjoin_proposal.to_json()) # self.payjoin.validate_payjoin_proposal() except (PayJoinProposalValidationException, PayJoinExchangeException) as e: _logger.warning(repr(e)) diff --git a/electrum/transaction.py b/electrum/transaction.py index 803e135e08af..80b1d59b7ac7 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -245,6 +245,11 @@ def witness_elements(self)-> Sequence[bytes]: n = vds.read_compact_size() return list(vds.read_bytes(vds.read_compact_size()) for i in range(n)) + def is_segwit(self, *, guess_for_address=False) -> bool: + if self.witness not in (b'\x00', b'', None): + return True + return False + class BCDataStream(object): """Workalike python implementation of Bitcoin's CDataStream class.""" @@ -1349,14 +1354,18 @@ def scriptpubkey(self) -> Optional[bytes]: return None def is_complete(self) -> bool: - if self.script_sig is not None or self.witness is not None: + if self.script_sig is not None and self.witness is not None: return True if self.is_coinbase_input(): return True - if self.script_sig is not None and not Transaction.is_segwit_input(self): + if self.script_sig is not None and not self.is_segwit(): + return True + if self.witness is not None and self.is_segwit(): return True + """ if self.witness is not None and Transaction.is_segwit_input(self): return True + """ signatures = list(self.part_sigs.values()) s = len(signatures) # note: The 'script_type' field is currently only set by the wallet, @@ -1457,6 +1466,20 @@ def calc_if_p2sh_segwit_now(): self._is_p2sh_segwit = calc_if_p2sh_segwit_now() return self._is_p2sh_segwit + def is_segwit(self, *, guess_for_address=False) -> bool: + if super().is_segwit(): + return True + if self.is_native_segwit() or self.is_p2sh_segwit(): + return True + if self.is_native_segwit() is False and self.is_p2sh_segwit() is False: + return False + if self.witness_script: + return True + _type = self.script_type + if _type == 'address' and guess_for_address: + _type = Transaction.guess_txintype_from_address(self.address) + return is_segwit_script_type(_type) + def already_has_some_signatures(self) -> bool: """Returns whether progress has been made towards completing this input.""" return (self.part_sigs @@ -1798,6 +1821,12 @@ def get_fee(self) -> Optional[int]: except MissingTxInputAmount: return None + def get_fee_rate(self) -> Optional[int]: + if self.get_fee() is None: + return None + return self.get_fee() / self.estimated_size() + + def serialize_preimage(self, txin_index: int, *, bip143_shared_txdigest_fields: BIP143SharedTxDigestFields = None) -> str: nVersion = int_to_hex(self.version, 4)