tMerge pull request #5011 from SomberNight/tx_broadcast_sanitize_response - electrum - Electrum Bitcoin wallet
 (HTM) git clone https://git.parazyd.org/electrum
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
       ---
 (DIR) commit c995d5364e47fc14b4d43dbfb940168be2eeabc9
 (DIR) parent 7ffd928e8052a59c33081cd463d0325304bd6bec
 (HTM) Author: ThomasV <thomasv@electrum.org>
       Date:   Sat, 19 Jan 2019 11:27:48 +0100
       
       Merge pull request #5011 from SomberNight/tx_broadcast_sanitize_response
       
       network: sanitize tx broadcast response
       Diffstat:
         M electrum/commands.py                |       3 ++-
         M electrum/gui/kivy/main_window.py    |      21 ++++++++++-----------
         M electrum/gui/qt/main_window.py      |      15 ++++++++-------
         M electrum/gui/stdio.py               |      11 +++++++----
         M electrum/gui/text.py                |      12 +++++++-----
         M electrum/network.py                 |     176 +++++++++++++++++++++++++++++--
       
       6 files changed, 204 insertions(+), 34 deletions(-)
       ---
 (DIR) diff --git a/electrum/commands.py b/electrum/commands.py
       t@@ -329,7 +329,8 @@ class Commands:
            def broadcast(self, tx):
                """Broadcast a transaction to the network. """
                tx = Transaction(tx)
       -        return self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
       +        self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
       +        return tx.txid()
        
            @command('')
            def createmultisig(self, num, pubkeys):
 (DIR) diff --git a/electrum/gui/kivy/main_window.py b/electrum/gui/kivy/main_window.py
       t@@ -16,7 +16,7 @@ from electrum.plugin import run_hook
        from electrum.util import format_satoshis, format_satoshis_plain
        from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
        from electrum import blockchain
       -from electrum.network import Network
       +from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
        from .i18n import _
        
        from kivy.app import App
       t@@ -917,14 +917,16 @@ class ElectrumWindow(App):
                Clock.schedule_once(lambda dt: on_success(tx))
        
            def _broadcast_thread(self, tx, on_complete):
       -
       +        status = False
                try:
                    self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
       -        except Exception as e:
       -            ok, msg = False, repr(e)
       +        except TxBroadcastError as e:
       +            msg = e.get_message_for_gui()
       +        except BestEffortRequestFailed as e:
       +            msg = repr(e)
                else:
       -            ok, msg = True, tx.txid()
       -        Clock.schedule_once(lambda dt: on_complete(ok, msg))
       +            status, msg = True, tx.txid()
       +        Clock.schedule_once(lambda dt: on_complete(status, msg))
        
            def broadcast(self, tx, pr=None):
                def on_complete(ok, msg):
       t@@ -937,11 +939,8 @@ class ElectrumWindow(App):
                            self.wallet.invoices.save()
                            self.update_tab('invoices')
                    else:
       -                display_msg = _('The server returned an error when broadcasting the transaction.')
       -                if msg:
       -                    display_msg += '\n' + msg
       -                display_msg = display_msg[:500]
       -                self.show_error(display_msg)
       +                msg = msg or ''
       +                self.show_error(msg)
        
                if self.network and self.network.is_connected():
                    self.show_info(_('Sending'))
 (DIR) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py
       t@@ -62,7 +62,7 @@ from electrum.address_synchronizer import AddTransactionException
        from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
                                     sweep_preparations, InternalAddressCorruption)
        from electrum.version import ELECTRUM_VERSION
       -from electrum.network import Network
       +from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
        from electrum.exchange_rate import FxThread
        from electrum.simple_config import SimpleConfig
        
       t@@ -1660,10 +1660,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                    if pr and pr.has_expired():
                        self.payment_request = None
                        return False, _("Payment request has expired")
       +            status = False
                    try:
                        self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
       -            except Exception as e:
       -                status, msg = False, repr(e)
       +            except TxBroadcastError as e:
       +                msg = e.get_message_for_gui()
       +            except BestEffortRequestFailed as e:
       +                msg = repr(e)
                    else:
                        status, msg = True, tx.txid()
                    if pr and status is True:
       t@@ -1691,10 +1694,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                            self.invoice_list.update()
                            self.do_clear()
                        else:
       -                    display_msg = _('The server returned an error when broadcasting the transaction.')
       -                    if msg:
       -                        display_msg += '\n' + msg
       -                    parent.show_error(display_msg)
       +                    msg = msg or ''
       +                    parent.show_error(msg)
        
                WaitingDialog(self, _('Broadcasting transaction...'),
                              broadcast_thread, broadcast_done, self.on_error)
 (DIR) diff --git a/electrum/gui/stdio.py b/electrum/gui/stdio.py
       t@@ -6,6 +6,7 @@ from electrum import WalletStorage, Wallet
        from electrum.util import format_satoshis, set_verbosity
        from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
        from electrum.transaction import TxOutput
       +from electrum.network import TxBroadcastError, BestEffortRequestFailed
        
        _ = lambda x:x  # i18n
        
       t@@ -205,10 +206,12 @@ class ElectrumGui:
                print(_("Please wait..."))
                try:
                    self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
       -        except Exception as e:
       -            display_msg = _('The server returned an error when broadcasting the transaction.')
       -            display_msg += '\n' + repr(e)
       -            print(display_msg)
       +        except TxBroadcastError as e:
       +            msg = e.get_message_for_gui()
       +            print(msg)
       +        except BestEffortRequestFailed as e:
       +            msg = repr(e)
       +            print(msg)
                else:
                    print(_('Payment sent.'))
                    #self.do_clear()
 (DIR) diff --git a/electrum/gui/text.py b/electrum/gui/text.py
       t@@ -12,7 +12,7 @@ from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
        from electrum.transaction import TxOutput
        from electrum.wallet import Wallet
        from electrum.storage import WalletStorage
       -from electrum.network import NetworkParameters
       +from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
        from electrum.interface import deserialize_server
        
        _ = lambda x:x  # i18n
       t@@ -369,10 +369,12 @@ class ElectrumGui:
                self.show_message(_("Please wait..."), getchar=False)
                try:
                    self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
       -        except Exception as e:
       -            display_msg = _('The server returned an error when broadcasting the transaction.')
       -            display_msg += '\n' + repr(e)
       -            self.show_message(display_msg)
       +        except TxBroadcastError as e:
       +            msg = e.get_message_for_gui()
       +            self.show_message(msg)
       +        except BestEffortRequestFailed as e:
       +            msg = repr(e)
       +            self.show_message(msg)
                else:
                    self.show_message(_('Payment sent.'))
                    self.do_clear()
 (DIR) diff --git a/electrum/network.py b/electrum/network.py
       t@@ -37,6 +37,7 @@ import traceback
        
        import dns
        import dns.resolver
       +import aiorpcx
        from aiorpcx import TaskGroup
        from aiohttp import ClientResponse
        
       t@@ -53,6 +54,7 @@ from .interface import (Interface, serialize_server, deserialize_server,
                                RequestTimedOut, NetworkTimeout)
        from .version import PROTOCOL_VERSION
        from .simple_config import SimpleConfig
       +from .i18n import _
        
        NODES_RETRY_INTERVAL = 60
        SERVER_RETRY_INTERVAL = 10
       t@@ -162,6 +164,30 @@ def deserialize_proxy(s: str) -> Optional[dict]:
            return proxy
        
        
       +class BestEffortRequestFailed(Exception): pass
       +
       +
       +class TxBroadcastError(Exception):
       +    def get_message_for_gui(self):
       +        raise NotImplementedError()
       +
       +
       +class TxBroadcastHashMismatch(TxBroadcastError):
       +    def get_message_for_gui(self):
       +        return "{}\n{}\n\n{}" \
       +            .format(_("The server returned an unexpected transaction ID when broadcasting the transaction."),
       +                    _("Consider trying to connect to a different server, or updating Electrum."),
       +                    str(self))
       +
       +
       +class TxBroadcastServerReturnedError(TxBroadcastError):
       +    def get_message_for_gui(self):
       +        return "{}\n{}\n\n{}" \
       +            .format(_("The server returned an error when broadcasting the transaction."),
       +                    _("Consider trying to connect to a different server, or updating Electrum."),
       +                    str(self))
       +
       +
        INSTANCE = None
        
        
       t@@ -724,7 +750,7 @@ class Network(PrintError):
                                    continue  # try again
                            return success_fut.result()
                        # otherwise; try again
       -            raise Exception('no interface to do request on... gave up.')
       +            raise BestEffortRequestFailed('no interface to do request on... gave up.')
                return make_reliable_wrapper
        
            @best_effort_reliable
       t@@ -732,14 +758,152 @@ class Network(PrintError):
                return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
        
            @best_effort_reliable
       -    async def broadcast_transaction(self, tx, *, timeout=None):
       +    async def broadcast_transaction(self, tx, *, timeout=None) -> None:
                if timeout is None:
                    timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent)
       -        out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout)
       +        try:
       +            out = await self.interface.session.send_request('blockchain.transaction.broadcast', [str(tx)], timeout=timeout)
       +            # note: both 'out' and exception messages are untrusted input from the server
       +        except aiorpcx.jsonrpc.RPCError as e:
       +            self.print_error(f"broadcast_transaction error: {repr(e)}")
       +            raise TxBroadcastServerReturnedError(self.sanitize_tx_broadcast_response(e.message)) from e
                if out != tx.txid():
       -            # note: this is untrusted input from the server
       -            raise Exception(out)
       -        return out  # txid
       +            self.print_error(f"unexpected txid for broadcast_transaction: {out} != {tx.txid()}")
       +            raise TxBroadcastHashMismatch(_("Server returned unexpected transaction ID."))
       +
       +    @staticmethod
       +    def sanitize_tx_broadcast_response(server_msg) -> str:
       +        # Unfortunately, bitcoind and hence the Electrum protocol doesn't return a useful error code.
       +        # So, we use substring matching to grok the error message.
       +        # server_msg is untrusted input so it should not be shown to the user. see #4968
       +        server_msg = str(server_msg)
       +        server_msg = server_msg.replace("\n", r"\n")
       +        # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/policy/policy.cpp
       +        # grep "reason ="
       +        policy_error_messages = {
       +            r"version": None,
       +            r"tx-size": _("The transaction was rejected because it is too large."),
       +            r"scriptsig-size": None,
       +            r"scriptsig-not-pushonly": None,
       +            r"scriptpubkey": None,
       +            r"bare-multisig": None,
       +            r"dust": _("Transaction could not be broadcast due to dust outputs."),
       +            r"multi-op-return": _("The transaction was rejected because it contains more than 1 OP_RETURN input."),
       +        }
       +        for substring in policy_error_messages:
       +            if substring in server_msg:
       +                msg = policy_error_messages[substring]
       +                return msg if msg else substring
       +        # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/script/script_error.cpp
       +        script_error_messages = {
       +            r"Script evaluated without error but finished with a false/empty top stack element",
       +            r"Script failed an OP_VERIFY operation",
       +            r"Script failed an OP_EQUALVERIFY operation",
       +            r"Script failed an OP_CHECKMULTISIGVERIFY operation",
       +            r"Script failed an OP_CHECKSIGVERIFY operation",
       +            r"Script failed an OP_NUMEQUALVERIFY operation",
       +            r"Script is too big",
       +            r"Push value size limit exceeded",
       +            r"Operation limit exceeded",
       +            r"Stack size limit exceeded",
       +            r"Signature count negative or greater than pubkey count",
       +            r"Pubkey count negative or limit exceeded",
       +            r"Opcode missing or not understood",
       +            r"Attempted to use a disabled opcode",
       +            r"Operation not valid with the current stack size",
       +            r"Operation not valid with the current altstack size",
       +            r"OP_RETURN was encountered",
       +            r"Invalid OP_IF construction",
       +            r"Negative locktime",
       +            r"Locktime requirement not satisfied",
       +            r"Signature hash type missing or not understood",
       +            r"Non-canonical DER signature",
       +            r"Data push larger than necessary",
       +            r"Only non-push operators allowed in signatures",
       +            r"Non-canonical signature: S value is unnecessarily high",
       +            r"Dummy CHECKMULTISIG argument must be zero",
       +            r"OP_IF/NOTIF argument must be minimal",
       +            r"Signature must be zero for failed CHECK(MULTI)SIG operation",
       +            r"NOPx reserved for soft-fork upgrades",
       +            r"Witness version reserved for soft-fork upgrades",
       +            r"Public key is neither compressed or uncompressed",
       +            r"Extra items left on stack after execution",
       +            r"Witness program has incorrect length",
       +            r"Witness program was passed an empty witness",
       +            r"Witness program hash mismatch",
       +            r"Witness requires empty scriptSig",
       +            r"Witness requires only-redeemscript scriptSig",
       +            r"Witness provided for non-witness script",
       +            r"Using non-compressed keys in segwit",
       +            r"Using OP_CODESEPARATOR in non-witness script",
       +            r"Signature is found in scriptCode",
       +        }
       +        for substring in script_error_messages:
       +            if substring in server_msg:
       +                return substring
       +        # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/validation.cpp
       +        # grep "REJECT_"
       +        # should come after script_error.cpp (due to e.g. non-mandatory-script-verify-flag)
       +        validation_error_messages = {
       +            r"coinbase",
       +            r"tx-size-small",
       +            r"non-final",
       +            r"txn-already-in-mempool",
       +            r"txn-mempool-conflict",
       +            r"txn-already-known",
       +            r"non-BIP68-final",
       +            r"bad-txns-nonstandard-inputs",
       +            r"bad-witness-nonstandard",
       +            r"bad-txns-too-many-sigops",
       +            r"mempool min fee not met",
       +            r"min relay fee not met",
       +            r"absurdly-high-fee",
       +            r"too-long-mempool-chain",
       +            r"bad-txns-spends-conflicting-tx",
       +            r"insufficient fee",
       +            r"too many potential replacements",
       +            r"replacement-adds-unconfirmed",
       +            r"mempool full",
       +            r"non-mandatory-script-verify-flag",
       +            r"mandatory-script-verify-flag-failed",
       +        }
       +        for substring in validation_error_messages:
       +            if substring in server_msg:
       +                return substring
       +        # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/rpc/rawtransaction.cpp
       +        # grep "RPC_TRANSACTION"
       +        # grep "RPC_DESERIALIZATION_ERROR"
       +        rawtransaction_error_messages = {
       +            r"Missing inputs",
       +            r"transaction already in block chain",
       +            r"TX decode failed",
       +        }
       +        for substring in rawtransaction_error_messages:
       +            if substring in server_msg:
       +                return substring
       +        # https://github.com/bitcoin/bitcoin/blob/cd42553b1178a48a16017eff0b70669c84c3895c/src/consensus/tx_verify.cpp
       +        # grep "REJECT_"
       +        tx_verify_error_messages = {
       +            r"bad-txns-vin-empty",
       +            r"bad-txns-vout-empty",
       +            r"bad-txns-oversize",
       +            r"bad-txns-vout-negative",
       +            r"bad-txns-vout-toolarge",
       +            r"bad-txns-txouttotal-toolarge",
       +            r"bad-txns-inputs-duplicate",
       +            r"bad-cb-length",
       +            r"bad-txns-prevout-null",
       +            r"bad-txns-inputs-missingorspent",
       +            r"bad-txns-premature-spend-of-coinbase",
       +            r"bad-txns-inputvalues-outofrange",
       +            r"bad-txns-in-belowout",
       +            r"bad-txns-fee-outofrange",
       +        }
       +        for substring in tx_verify_error_messages:
       +            if substring in server_msg:
       +                return substring
       +        # otherwise:
       +        return _("Unknown error")
        
            @best_effort_reliable
            async def request_chunk(self, height, tip=None, *, can_return_early=False):