1
0

interface: don't request same tx from server that we just broadcast to it

Often when the wallet creates a tx, the flow is:
- create unsigned tx
- sign tx
- broadcast tx, but don't save it in history
- server sends notification that status of a subscribed address changed
- client calls scripthash.get_history
- client sees txid in scripthash.get_history response
- client calls blockchain.transaction.get to request missing tx

Instead, now when we broadcast a tx on an interface, we cache that tx *for that interface*,
and just before calling blockchain.transaction.get, we lookup in the cache.
Hence this will often save a network request.
This commit is contained in:
SomberNight
2025-08-06 13:49:43 +00:00
parent 427b0d42b6
commit 05da50178b
2 changed files with 26 additions and 3 deletions

View File

@@ -64,6 +64,7 @@ from .i18n import _
from .logging import Logger
from .transaction import Transaction
from .fee_policy import FEE_ETA_TARGETS
from .lrucache import LRUCache
if TYPE_CHECKING:
from .network import Network
@@ -558,6 +559,7 @@ class Interface(Logger):
self.tip = 0
self._headers_cache = {} # type: Dict[int, bytes]
self._rawtx_cache = LRUCache(maxsize=20) # type: LRUCache[str, bytes] # txid->rawtx
self.fee_estimates_eta = {} # type: Dict[int, int]
@@ -1318,6 +1320,8 @@ class Interface(Logger):
async def get_transaction(self, tx_hash: str, *, timeout=None) -> str:
if not is_hash256_str(tx_hash):
raise Exception(f"{repr(tx_hash)} is not a txid")
if rawtx_bytes := self._rawtx_cache.get(tx_hash):
return rawtx_bytes.hex()
raw = await self.session.send_request('blockchain.transaction.get', [tx_hash], timeout=timeout)
# validate response
if not is_hex_str(raw):
@@ -1329,16 +1333,21 @@ class Interface(Logger):
raise RequestCorrupted(f"cannot deserialize received transaction (txid {tx_hash})") from e
if tx.txid() != tx_hash:
raise RequestCorrupted(f"received tx does not match expected txid {tx_hash} (got {tx.txid()})")
self._rawtx_cache[tx_hash] = bytes.fromhex(raw)
return raw
async def broadcast_transaction(self, tx: 'Transaction', *, timeout=None) -> None:
"""caller should handle TxBroadcastError and RequestTimedOut"""
txid_calc = tx.txid()
assert txid_calc is not None
rawtx = tx.serialize()
assert is_hex_str(rawtx)
if timeout is None:
timeout = self.network.get_network_timeout_seconds(NetworkTimeout.Urgent)
if any(DummyAddress.is_dummy_address(txout.address) for txout in tx.outputs()):
raise DummyAddressUsedInTxException("tried to broadcast tx with dummy address!")
try:
out = await self.session.send_request('blockchain.transaction.broadcast', [tx.serialize()], timeout=timeout)
out = await self.session.send_request('blockchain.transaction.broadcast', [rawtx], timeout=timeout)
# note: both 'out' and exception messages are untrusted input from the server
except (RequestTimedOut, asyncio.CancelledError, asyncio.TimeoutError):
raise # pass-through
@@ -1349,10 +1358,14 @@ class Interface(Logger):
self.logger.info(f"broadcast_transaction error2 [DO NOT TRUST THIS MESSAGE]: {error_text_str_to_safe_str(repr(e))}. tx={str(tx)}")
send_exception_to_crash_reporter(e)
raise TxBroadcastUnknownError() from e
if out != tx.txid():
if out != txid_calc:
self.logger.info(f"unexpected txid for broadcast_transaction [DO NOT TRUST THIS MESSAGE]: "
f"{error_text_str_to_safe_str(out)} != {tx.txid()}. tx={str(tx)}")
f"{error_text_str_to_safe_str(out)} != {txid_calc}. tx={str(tx)}")
raise TxBroadcastHashMismatch(_("Server returned unexpected transaction ID."))
# broadcast succeeded.
# We now cache the rawtx, for *this interface only*. The tx likely touches some ismine addresses, affecting
# the status of a scripthash we are subscribed to. Caching here will save a future get_transaction RPC.
self._rawtx_cache[txid_calc] = bytes.fromhex(rawtx)
async def get_history_for_scripthash(self, sh: str) -> List[dict]:
if not is_hash256_str(sh):