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):