4949
5050from .util import (ignore_exceptions , log_exceptions , bfh , ESocksProxy ,
5151 is_integer , is_non_negative_integer , is_hash256_str , is_hex_str ,
52- is_int_or_float , is_non_negative_int_or_float , OldTaskGroup )
52+ is_int_or_float , is_non_negative_int_or_float , OldTaskGroup ,
53+ send_exception_to_crash_reporter , error_text_str_to_safe_str )
5354from . import util
5455from . import x509
5556from . import pem
5657from . import version
5758from . import blockchain
5859from .blockchain import Blockchain , HEADER_SIZE , CHUNK_SIZE
5960from . import bitcoin
61+ from .bitcoin import DummyAddress , DummyAddressUsedInTxException
6062from . import constants
6163from .i18n import _
6264from .logging import Logger
6365from .transaction import Transaction
6466from .fee_policy import FEE_ETA_TARGETS
67+ from .lrucache import LRUCache
6568
6669if TYPE_CHECKING :
6770 from .network import Network
@@ -279,6 +282,34 @@ class InvalidOptionCombination(Exception): pass
279282class ConnectError (NetworkException ): pass
280283
281284
285+ class TxBroadcastError (NetworkException ):
286+ def get_message_for_gui (self ):
287+ raise NotImplementedError ()
288+
289+
290+ class TxBroadcastHashMismatch (TxBroadcastError ):
291+ def get_message_for_gui (self ):
292+ return "{}\n {}\n \n {}" \
293+ .format (_ ("The server returned an unexpected transaction ID when broadcasting the transaction." ),
294+ _ ("Consider trying to connect to a different server, or updating Electrum." ),
295+ str (self ))
296+
297+
298+ class TxBroadcastServerReturnedError (TxBroadcastError ):
299+ def get_message_for_gui (self ):
300+ return "{}\n {}\n \n {}" \
301+ .format (_ ("The server returned an error when broadcasting the transaction." ),
302+ _ ("Consider trying to connect to a different server, or updating Electrum." ),
303+ str (self ))
304+
305+
306+ class TxBroadcastUnknownError (TxBroadcastError ):
307+ def get_message_for_gui (self ):
308+ return "{}\n {}" \
309+ .format (_ ("Unknown error when broadcasting the transaction." ),
310+ _ ("Consider trying to connect to a different server, or updating Electrum." ))
311+
312+
282313class _RSClient (RSClient ):
283314 async def create_connection (self ):
284315 try :
@@ -499,6 +530,7 @@ def __init__(self, *, network: 'Network', server: ServerAddr):
499530 assert isinstance (server , ServerAddr ), f"expected ServerAddr, got { type (server )} "
500531 self .ready = network .asyncio_loop .create_future ()
501532 self .got_disconnected = asyncio .Event ()
533+ self ._blockchain_updated = asyncio .Event ()
502534 self .server = server
503535 Logger .__init__ (self )
504536 assert network .config .path
@@ -527,6 +559,7 @@ def __init__(self, *, network: 'Network', server: ServerAddr):
527559 self .tip = 0
528560
529561 self ._headers_cache = {} # type: Dict[int, bytes]
562+ self ._rawtx_cache = LRUCache (maxsize = 20 ) # type: LRUCache[str, bytes] # txid->rawtx
530563
531564 self .fee_estimates_eta = {} # type: Dict[int, int]
532565
@@ -1028,6 +1061,8 @@ async def run_fetch_blocks(self):
10281061 self .logger .info (f"new chain tip. { height = } " )
10291062 if blockchain_updated :
10301063 util .trigger_callback ('blockchain_updated' )
1064+ self ._blockchain_updated .set ()
1065+ self ._blockchain_updated .clear ()
10311066 util .trigger_callback ('network_updated' )
10321067 await self .network .switch_unwanted_fork_interface ()
10331068 await self .network .switch_lagging_interface ()
@@ -1070,6 +1105,8 @@ async def sync_until(
10701105 continue
10711106 # report progress to gui/etc
10721107 util .trigger_callback ('blockchain_updated' )
1108+ self ._blockchain_updated .set ()
1109+ self ._blockchain_updated .clear ()
10731110 util .trigger_callback ('network_updated' )
10741111 height += num_headers
10751112 assert height <= next_height + 1 , (height , self .tip )
@@ -1283,6 +1320,8 @@ async def get_merkle_for_transaction(self, tx_hash: str, tx_height: int) -> dict
12831320 async def get_transaction (self , tx_hash : str , * , timeout = None ) -> str :
12841321 if not is_hash256_str (tx_hash ):
12851322 raise Exception (f"{ repr (tx_hash )} is not a txid" )
1323+ if rawtx_bytes := self ._rawtx_cache .get (tx_hash ):
1324+ return rawtx_bytes .hex ()
12861325 raw = await self .session .send_request ('blockchain.transaction.get' , [tx_hash ], timeout = timeout )
12871326 # validate response
12881327 if not is_hex_str (raw ):
@@ -1294,8 +1333,40 @@ async def get_transaction(self, tx_hash: str, *, timeout=None) -> str:
12941333 raise RequestCorrupted (f"cannot deserialize received transaction (txid { tx_hash } )" ) from e
12951334 if tx .txid () != tx_hash :
12961335 raise RequestCorrupted (f"received tx does not match expected txid { tx_hash } (got { tx .txid ()} )" )
1336+ self ._rawtx_cache [tx_hash ] = bytes .fromhex (raw )
12971337 return raw
12981338
1339+ async def broadcast_transaction (self , tx : 'Transaction' , * , timeout = None ) -> None :
1340+ """caller should handle TxBroadcastError and RequestTimedOut"""
1341+ txid_calc = tx .txid ()
1342+ assert txid_calc is not None
1343+ rawtx = tx .serialize ()
1344+ assert is_hex_str (rawtx )
1345+ if timeout is None :
1346+ timeout = self .network .get_network_timeout_seconds (NetworkTimeout .Urgent )
1347+ if any (DummyAddress .is_dummy_address (txout .address ) for txout in tx .outputs ()):
1348+ raise DummyAddressUsedInTxException ("tried to broadcast tx with dummy address!" )
1349+ try :
1350+ out = await self .session .send_request ('blockchain.transaction.broadcast' , [rawtx ], timeout = timeout )
1351+ # note: both 'out' and exception messages are untrusted input from the server
1352+ except (RequestTimedOut , asyncio .CancelledError , asyncio .TimeoutError ):
1353+ raise # pass-through
1354+ except aiorpcx .jsonrpc .CodeMessageError as e :
1355+ self .logger .info (f"broadcast_transaction error [DO NOT TRUST THIS MESSAGE]: { error_text_str_to_safe_str (repr (e ))} . tx={ str (tx )} " )
1356+ raise TxBroadcastServerReturnedError (sanitize_tx_broadcast_response (e .message )) from e
1357+ except BaseException as e : # intentional BaseException for sanity!
1358+ self .logger .info (f"broadcast_transaction error2 [DO NOT TRUST THIS MESSAGE]: { error_text_str_to_safe_str (repr (e ))} . tx={ str (tx )} " )
1359+ send_exception_to_crash_reporter (e )
1360+ raise TxBroadcastUnknownError () from e
1361+ if out != txid_calc :
1362+ self .logger .info (f"unexpected txid for broadcast_transaction [DO NOT TRUST THIS MESSAGE]: "
1363+ f"{ error_text_str_to_safe_str (out )} != { txid_calc } . tx={ str (tx )} " )
1364+ raise TxBroadcastHashMismatch (_ ("Server returned unexpected transaction ID." ))
1365+ # broadcast succeeded.
1366+ # We now cache the rawtx, for *this interface only*. The tx likely touches some ismine addresses, affecting
1367+ # the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.
1368+ self ._rawtx_cache [txid_calc ] = bytes .fromhex (rawtx )
1369+
12991370 async def get_history_for_scripthash (self , sh : str ) -> List [dict ]:
13001371 if not is_hash256_str (sh ):
13011372 raise Exception (f"{ repr (sh )} is not a scripthash" )
@@ -1462,6 +1533,190 @@ def _assert_header_does_not_check_against_any_chain(header: dict) -> None:
14621533 raise Exception ('bad_header must not check!' )
14631534
14641535
1536+ def sanitize_tx_broadcast_response (server_msg ) -> str :
1537+ # Unfortunately, bitcoind and hence the Electrum protocol doesn't return a useful error code.
1538+ # So, we use substring matching to grok the error message.
1539+ # server_msg is untrusted input so it should not be shown to the user. see #4968
1540+ server_msg = str (server_msg )
1541+ server_msg = server_msg .replace ("\n " , r"\n" )
1542+
1543+ # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/script/script_error.cpp
1544+ script_error_messages = {
1545+ r"Script evaluated without error but finished with a false/empty top stack element" ,
1546+ r"Script failed an OP_VERIFY operation" ,
1547+ r"Script failed an OP_EQUALVERIFY operation" ,
1548+ r"Script failed an OP_CHECKMULTISIGVERIFY operation" ,
1549+ r"Script failed an OP_CHECKSIGVERIFY operation" ,
1550+ r"Script failed an OP_NUMEQUALVERIFY operation" ,
1551+ r"Script is too big" ,
1552+ r"Push value size limit exceeded" ,
1553+ r"Operation limit exceeded" ,
1554+ r"Stack size limit exceeded" ,
1555+ r"Signature count negative or greater than pubkey count" ,
1556+ r"Pubkey count negative or limit exceeded" ,
1557+ r"Opcode missing or not understood" ,
1558+ r"Attempted to use a disabled opcode" ,
1559+ r"Operation not valid with the current stack size" ,
1560+ r"Operation not valid with the current altstack size" ,
1561+ r"OP_RETURN was encountered" ,
1562+ r"Invalid OP_IF construction" ,
1563+ r"Negative locktime" ,
1564+ r"Locktime requirement not satisfied" ,
1565+ r"Signature hash type missing or not understood" ,
1566+ r"Non-canonical DER signature" ,
1567+ r"Data push larger than necessary" ,
1568+ r"Only push operators allowed in signatures" ,
1569+ r"Non-canonical signature: S value is unnecessarily high" ,
1570+ r"Dummy CHECKMULTISIG argument must be zero" ,
1571+ r"OP_IF/NOTIF argument must be minimal" ,
1572+ r"Signature must be zero for failed CHECK(MULTI)SIG operation" ,
1573+ r"NOPx reserved for soft-fork upgrades" ,
1574+ r"Witness version reserved for soft-fork upgrades" ,
1575+ r"Taproot version reserved for soft-fork upgrades" ,
1576+ r"OP_SUCCESSx reserved for soft-fork upgrades" ,
1577+ r"Public key version reserved for soft-fork upgrades" ,
1578+ r"Public key is neither compressed or uncompressed" ,
1579+ r"Stack size must be exactly one after execution" ,
1580+ r"Extra items left on stack after execution" ,
1581+ r"Witness program has incorrect length" ,
1582+ r"Witness program was passed an empty witness" ,
1583+ r"Witness program hash mismatch" ,
1584+ r"Witness requires empty scriptSig" ,
1585+ r"Witness requires only-redeemscript scriptSig" ,
1586+ r"Witness provided for non-witness script" ,
1587+ r"Using non-compressed keys in segwit" ,
1588+ r"Invalid Schnorr signature size" ,
1589+ r"Invalid Schnorr signature hash type" ,
1590+ r"Invalid Schnorr signature" ,
1591+ r"Invalid Taproot control block size" ,
1592+ r"Too much signature validation relative to witness weight" ,
1593+ r"OP_CHECKMULTISIG(VERIFY) is not available in tapscript" ,
1594+ r"OP_IF/NOTIF argument must be minimal in tapscript" ,
1595+ r"Using OP_CODESEPARATOR in non-witness script" ,
1596+ r"Signature is found in scriptCode" ,
1597+ }
1598+ for substring in script_error_messages :
1599+ if substring in server_msg :
1600+ return substring
1601+ # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/validation.cpp
1602+ # grep "REJECT_"
1603+ # grep "TxValidationResult"
1604+ # should come after script_error.cpp (due to e.g. "non-mandatory-script-verify-flag")
1605+ validation_error_messages = {
1606+ r"coinbase" : None ,
1607+ r"tx-size-small" : None ,
1608+ r"non-final" : None ,
1609+ r"txn-already-in-mempool" : None ,
1610+ r"txn-mempool-conflict" : None ,
1611+ r"txn-already-known" : None ,
1612+ r"non-BIP68-final" : None ,
1613+ r"bad-txns-nonstandard-inputs" : None ,
1614+ r"bad-witness-nonstandard" : None ,
1615+ r"bad-txns-too-many-sigops" : None ,
1616+ r"mempool min fee not met" :
1617+ ("mempool min fee not met\n " +
1618+ _ ("Your transaction is paying a fee that is so low that the bitcoin node cannot "
1619+ "fit it into its mempool. The mempool is already full of hundreds of megabytes "
1620+ "of transactions that all pay higher fees. Try to increase the fee." )),
1621+ r"min relay fee not met" : None ,
1622+ r"absurdly-high-fee" : None ,
1623+ r"max-fee-exceeded" : None ,
1624+ r"too-long-mempool-chain" : None ,
1625+ r"bad-txns-spends-conflicting-tx" : None ,
1626+ r"insufficient fee" : ("insufficient fee\n " +
1627+ _ ("Your transaction is trying to replace another one in the mempool but it "
1628+ "does not meet the rules to do so. Try to increase the fee." )),
1629+ r"too many potential replacements" : None ,
1630+ r"replacement-adds-unconfirmed" : None ,
1631+ r"mempool full" : None ,
1632+ r"non-mandatory-script-verify-flag" : None ,
1633+ r"mandatory-script-verify-flag-failed" : None ,
1634+ r"Transaction check failed" : None ,
1635+ }
1636+ for substring in validation_error_messages :
1637+ if substring in server_msg :
1638+ msg = validation_error_messages [substring ]
1639+ return msg if msg else substring
1640+ # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/rpc/rawtransaction.cpp
1641+ # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/util/error.cpp
1642+ # https://github.com/bitcoin/bitcoin/blob/3f83c744ac28b700090e15b5dda2260724a56f49/src/common/messages.cpp#L126
1643+ # grep "RPC_TRANSACTION"
1644+ # grep "RPC_DESERIALIZATION_ERROR"
1645+ # grep "TransactionError"
1646+ rawtransaction_error_messages = {
1647+ r"Missing inputs" : None ,
1648+ r"Inputs missing or spent" : None ,
1649+ r"transaction already in block chain" : None ,
1650+ r"Transaction already in block chain" : None ,
1651+ r"Transaction outputs already in utxo set" : None ,
1652+ r"TX decode failed" : None ,
1653+ r"Peer-to-peer functionality missing or disabled" : None ,
1654+ r"Transaction rejected by AcceptToMemoryPool" : None ,
1655+ r"AcceptToMemoryPool failed" : None ,
1656+ r"Transaction rejected by mempool" : None ,
1657+ r"Mempool internal error" : None ,
1658+ r"Fee exceeds maximum configured by user" : None ,
1659+ r"Unspendable output exceeds maximum configured by user" : None ,
1660+ r"Transaction rejected due to invalid package" : None ,
1661+ }
1662+ for substring in rawtransaction_error_messages :
1663+ if substring in server_msg :
1664+ msg = rawtransaction_error_messages [substring ]
1665+ return msg if msg else substring
1666+ # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/consensus/tx_verify.cpp
1667+ # https://github.com/bitcoin/bitcoin/blob/c7ad94428ab6f54661d7a5441e1fdd0ebf034903/src/consensus/tx_check.cpp
1668+ # grep "REJECT_"
1669+ # grep "TxValidationResult"
1670+ tx_verify_error_messages = {
1671+ r"bad-txns-vin-empty" : None ,
1672+ r"bad-txns-vout-empty" : None ,
1673+ r"bad-txns-oversize" : None ,
1674+ r"bad-txns-vout-negative" : None ,
1675+ r"bad-txns-vout-toolarge" : None ,
1676+ r"bad-txns-txouttotal-toolarge" : None ,
1677+ r"bad-txns-inputs-duplicate" : None ,
1678+ r"bad-cb-length" : None ,
1679+ r"bad-txns-prevout-null" : None ,
1680+ r"bad-txns-inputs-missingorspent" :
1681+ ("bad-txns-inputs-missingorspent\n " +
1682+ _ ("You might have a local transaction in your wallet that this transaction "
1683+ "builds on top. You need to either broadcast or remove the local tx." )),
1684+ r"bad-txns-premature-spend-of-coinbase" : None ,
1685+ r"bad-txns-inputvalues-outofrange" : None ,
1686+ r"bad-txns-in-belowout" : None ,
1687+ r"bad-txns-fee-outofrange" : None ,
1688+ }
1689+ for substring in tx_verify_error_messages :
1690+ if substring in server_msg :
1691+ msg = tx_verify_error_messages [substring ]
1692+ return msg if msg else substring
1693+ # https://github.com/bitcoin/bitcoin/blob/5bb64acd9d3ced6e6f95df282a1a0f8b98522cb0/src/policy/policy.cpp
1694+ # grep "reason ="
1695+ # should come after validation.cpp (due to "tx-size" vs "tx-size-small")
1696+ # should come after script_error.cpp (due to e.g. "version")
1697+ policy_error_messages = {
1698+ r"version" : _ ("Transaction uses non-standard version." ),
1699+ r"tx-size" : _ ("The transaction was rejected because it is too large (in bytes)." ),
1700+ r"scriptsig-size" : None ,
1701+ r"scriptsig-not-pushonly" : None ,
1702+ r"scriptpubkey" :
1703+ ("scriptpubkey\n " +
1704+ _ ("Some of the outputs pay to a non-standard script." )),
1705+ r"bare-multisig" : None ,
1706+ r"dust" :
1707+ (_ ("Transaction could not be broadcast due to dust outputs.\n "
1708+ "Some of the outputs are too small in value, probably lower than 1000 satoshis.\n "
1709+ "Check the units, make sure you haven't confused e.g. mBTC and BTC." )),
1710+ r"multi-op-return" : _ ("The transaction was rejected because it contains multiple OP_RETURN outputs." ),
1711+ }
1712+ for substring in policy_error_messages :
1713+ if substring in server_msg :
1714+ msg = policy_error_messages [substring ]
1715+ return msg if msg else substring
1716+ # otherwise:
1717+ return _ ("Unknown error" )
1718+
1719+
14651720def check_cert (host , cert ):
14661721 try :
14671722 b = pem .dePem (cert , 'CERTIFICATE' )
0 commit comments