mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
Merge pull request #5011 from SomberNight/tx_broadcast_sanitize_response
network: sanitize tx broadcast response
This commit is contained in:
commit
c995d5364e
6 changed files with 204 additions and 34 deletions
|
@ -329,7 +329,8 @@ class Commands:
|
||||||
def broadcast(self, tx):
|
def broadcast(self, tx):
|
||||||
"""Broadcast a transaction to the network. """
|
"""Broadcast a transaction to the network. """
|
||||||
tx = Transaction(tx)
|
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('')
|
@command('')
|
||||||
def createmultisig(self, num, pubkeys):
|
def createmultisig(self, num, pubkeys):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from electrum.plugin import run_hook
|
||||||
from electrum.util import format_satoshis, format_satoshis_plain
|
from electrum.util import format_satoshis, format_satoshis_plain
|
||||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
||||||
from electrum import blockchain
|
from electrum import blockchain
|
||||||
from electrum.network import Network
|
from electrum.network import Network, TxBroadcastError, BestEffortRequestFailed
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
|
||||||
from kivy.app import App
|
from kivy.app import App
|
||||||
|
@ -917,14 +917,16 @@ class ElectrumWindow(App):
|
||||||
Clock.schedule_once(lambda dt: on_success(tx))
|
Clock.schedule_once(lambda dt: on_success(tx))
|
||||||
|
|
||||||
def _broadcast_thread(self, tx, on_complete):
|
def _broadcast_thread(self, tx, on_complete):
|
||||||
|
status = False
|
||||||
try:
|
try:
|
||||||
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
||||||
except Exception as e:
|
except TxBroadcastError as e:
|
||||||
ok, msg = False, repr(e)
|
msg = e.get_message_for_gui()
|
||||||
|
except BestEffortRequestFailed as e:
|
||||||
|
msg = repr(e)
|
||||||
else:
|
else:
|
||||||
ok, msg = True, tx.txid()
|
status, msg = True, tx.txid()
|
||||||
Clock.schedule_once(lambda dt: on_complete(ok, msg))
|
Clock.schedule_once(lambda dt: on_complete(status, msg))
|
||||||
|
|
||||||
def broadcast(self, tx, pr=None):
|
def broadcast(self, tx, pr=None):
|
||||||
def on_complete(ok, msg):
|
def on_complete(ok, msg):
|
||||||
|
@ -937,11 +939,8 @@ class ElectrumWindow(App):
|
||||||
self.wallet.invoices.save()
|
self.wallet.invoices.save()
|
||||||
self.update_tab('invoices')
|
self.update_tab('invoices')
|
||||||
else:
|
else:
|
||||||
display_msg = _('The server returned an error when broadcasting the transaction.')
|
msg = msg or ''
|
||||||
if msg:
|
self.show_error(msg)
|
||||||
display_msg += '\n' + msg
|
|
||||||
display_msg = display_msg[:500]
|
|
||||||
self.show_error(display_msg)
|
|
||||||
|
|
||||||
if self.network and self.network.is_connected():
|
if self.network and self.network.is_connected():
|
||||||
self.show_info(_('Sending'))
|
self.show_info(_('Sending'))
|
||||||
|
|
|
@ -62,7 +62,7 @@ from electrum.address_synchronizer import AddTransactionException
|
||||||
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
|
from electrum.wallet import (Multisig_Wallet, CannotBumpFee, Abstract_Wallet,
|
||||||
sweep_preparations, InternalAddressCorruption)
|
sweep_preparations, InternalAddressCorruption)
|
||||||
from electrum.version import ELECTRUM_VERSION
|
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.exchange_rate import FxThread
|
||||||
from electrum.simple_config import SimpleConfig
|
from electrum.simple_config import SimpleConfig
|
||||||
|
|
||||||
|
@ -1660,10 +1660,13 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
if pr and pr.has_expired():
|
if pr and pr.has_expired():
|
||||||
self.payment_request = None
|
self.payment_request = None
|
||||||
return False, _("Payment request has expired")
|
return False, _("Payment request has expired")
|
||||||
|
status = False
|
||||||
try:
|
try:
|
||||||
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
||||||
except Exception as e:
|
except TxBroadcastError as e:
|
||||||
status, msg = False, repr(e)
|
msg = e.get_message_for_gui()
|
||||||
|
except BestEffortRequestFailed as e:
|
||||||
|
msg = repr(e)
|
||||||
else:
|
else:
|
||||||
status, msg = True, tx.txid()
|
status, msg = True, tx.txid()
|
||||||
if pr and status is True:
|
if pr and status is True:
|
||||||
|
@ -1691,10 +1694,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.invoice_list.update()
|
self.invoice_list.update()
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
else:
|
else:
|
||||||
display_msg = _('The server returned an error when broadcasting the transaction.')
|
msg = msg or ''
|
||||||
if msg:
|
parent.show_error(msg)
|
||||||
display_msg += '\n' + msg
|
|
||||||
parent.show_error(display_msg)
|
|
||||||
|
|
||||||
WaitingDialog(self, _('Broadcasting transaction...'),
|
WaitingDialog(self, _('Broadcasting transaction...'),
|
||||||
broadcast_thread, broadcast_done, self.on_error)
|
broadcast_thread, broadcast_done, self.on_error)
|
||||||
|
|
|
@ -6,6 +6,7 @@ from electrum import WalletStorage, Wallet
|
||||||
from electrum.util import format_satoshis, set_verbosity
|
from electrum.util import format_satoshis, set_verbosity
|
||||||
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
|
from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
|
||||||
from electrum.transaction import TxOutput
|
from electrum.transaction import TxOutput
|
||||||
|
from electrum.network import TxBroadcastError, BestEffortRequestFailed
|
||||||
|
|
||||||
_ = lambda x:x # i18n
|
_ = lambda x:x # i18n
|
||||||
|
|
||||||
|
@ -205,10 +206,12 @@ class ElectrumGui:
|
||||||
print(_("Please wait..."))
|
print(_("Please wait..."))
|
||||||
try:
|
try:
|
||||||
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
||||||
except Exception as e:
|
except TxBroadcastError as e:
|
||||||
display_msg = _('The server returned an error when broadcasting the transaction.')
|
msg = e.get_message_for_gui()
|
||||||
display_msg += '\n' + repr(e)
|
print(msg)
|
||||||
print(display_msg)
|
except BestEffortRequestFailed as e:
|
||||||
|
msg = repr(e)
|
||||||
|
print(msg)
|
||||||
else:
|
else:
|
||||||
print(_('Payment sent.'))
|
print(_('Payment sent.'))
|
||||||
#self.do_clear()
|
#self.do_clear()
|
||||||
|
|
|
@ -12,7 +12,7 @@ from electrum.bitcoin import is_address, COIN, TYPE_ADDRESS
|
||||||
from electrum.transaction import TxOutput
|
from electrum.transaction import TxOutput
|
||||||
from electrum.wallet import Wallet
|
from electrum.wallet import Wallet
|
||||||
from electrum.storage import WalletStorage
|
from electrum.storage import WalletStorage
|
||||||
from electrum.network import NetworkParameters
|
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed
|
||||||
from electrum.interface import deserialize_server
|
from electrum.interface import deserialize_server
|
||||||
|
|
||||||
_ = lambda x:x # i18n
|
_ = lambda x:x # i18n
|
||||||
|
@ -369,10 +369,12 @@ class ElectrumGui:
|
||||||
self.show_message(_("Please wait..."), getchar=False)
|
self.show_message(_("Please wait..."), getchar=False)
|
||||||
try:
|
try:
|
||||||
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
||||||
except Exception as e:
|
except TxBroadcastError as e:
|
||||||
display_msg = _('The server returned an error when broadcasting the transaction.')
|
msg = e.get_message_for_gui()
|
||||||
display_msg += '\n' + repr(e)
|
self.show_message(msg)
|
||||||
self.show_message(display_msg)
|
except BestEffortRequestFailed as e:
|
||||||
|
msg = repr(e)
|
||||||
|
self.show_message(msg)
|
||||||
else:
|
else:
|
||||||
self.show_message(_('Payment sent.'))
|
self.show_message(_('Payment sent.'))
|
||||||
self.do_clear()
|
self.do_clear()
|
||||||
|
|
|
@ -37,6 +37,7 @@ import traceback
|
||||||
|
|
||||||
import dns
|
import dns
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
|
import aiorpcx
|
||||||
from aiorpcx import TaskGroup
|
from aiorpcx import TaskGroup
|
||||||
from aiohttp import ClientResponse
|
from aiohttp import ClientResponse
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ from .interface import (Interface, serialize_server, deserialize_server,
|
||||||
RequestTimedOut, NetworkTimeout)
|
RequestTimedOut, NetworkTimeout)
|
||||||
from .version import PROTOCOL_VERSION
|
from .version import PROTOCOL_VERSION
|
||||||
from .simple_config import SimpleConfig
|
from .simple_config import SimpleConfig
|
||||||
|
from .i18n import _
|
||||||
|
|
||||||
NODES_RETRY_INTERVAL = 60
|
NODES_RETRY_INTERVAL = 60
|
||||||
SERVER_RETRY_INTERVAL = 10
|
SERVER_RETRY_INTERVAL = 10
|
||||||
|
@ -162,6 +164,30 @@ def deserialize_proxy(s: str) -> Optional[dict]:
|
||||||
return proxy
|
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
|
INSTANCE = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -724,7 +750,7 @@ class Network(PrintError):
|
||||||
continue # try again
|
continue # try again
|
||||||
return success_fut.result()
|
return success_fut.result()
|
||||||
# otherwise; try again
|
# 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
|
return make_reliable_wrapper
|
||||||
|
|
||||||
@best_effort_reliable
|
@best_effort_reliable
|
||||||
|
@ -732,14 +758,152 @@ class Network(PrintError):
|
||||||
return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
|
return await self.interface.session.send_request('blockchain.transaction.get_merkle', [tx_hash, tx_height])
|
||||||
|
|
||||||
@best_effort_reliable
|
@best_effort_reliable
|
||||||
async def broadcast_transaction(self, tx, *, timeout=None):
|
async def broadcast_transaction(self, tx, *, timeout=None) -> None:
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = self.get_network_timeout_seconds(NetworkTimeout.Urgent)
|
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():
|
if out != tx.txid():
|
||||||
# note: this is untrusted input from the server
|
self.print_error(f"unexpected txid for broadcast_transaction: {out} != {tx.txid()}")
|
||||||
raise Exception(out)
|
raise TxBroadcastHashMismatch(_("Server returned unexpected transaction ID."))
|
||||||
return out # txid
|
|
||||||
|
@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
|
@best_effort_reliable
|
||||||
async def request_chunk(self, height, tip=None, *, can_return_early=False):
|
async def request_chunk(self, height, tip=None, *, can_return_early=False):
|
||||||
|
|
Loading…
Add table
Reference in a new issue