@@ -423,6 +423,8 @@ def __init__(self, db: WalletDB, *, config: SimpleConfig):
423423 self ._last_full_history = None
424424 self ._tx_parents_cache = {}
425425 self ._paid_invoice_keys_cache = set () # type: Set[str]
426+ # only set while an _update_onchain_invoice_paid_detection pass runs, see there
427+ self ._invoice_prevouts_cache = None # type: Optional[Dict[bytes, Sequence[Tuple[str, int, int, int]]]]
426428 self ._coin_price_cache = {}
427429 self ._default_labels = {}
428430 self ._accounting_addresses = set () # addresses counted as ours after successful sweep
@@ -1376,33 +1378,39 @@ def _prepare_onchain_invoice_paid_detection(self):
13761378 self ._update_onchain_invoice_paid_detection (self ._invoices .keys ())
13771379
13781380 def _update_onchain_invoice_paid_detection (self , invoice_keys : Iterable [str ]) -> None :
1379- for invoice_key in invoice_keys :
1380- invoice = self ._invoices .get (invoice_key )
1381- if not invoice :
1382- continue
1383- # clear the cache first so self.get_invoice_status takes the slow path,
1384- # which is needed to detect paid->unpaid transitions (e.g. reorgs)
1385- self ._paid_invoice_keys_cache .discard (invoice_key )
1386- if invoice .is_lightning ():
1387- is_paid_lightning = bool (self .lnworker and self .lnworker .get_invoice_status (invoice ) == PR_PAID )
1388- if is_paid_lightning :
1389- self ._paid_invoice_keys_cache .add (invoice_key )
1390- if is_paid_lightning or not invoice .get_address ():
1381+ # invoices often share scriptpubkeys; share the prevout lookups across this pass,
1382+ # including the ones done by self.get_invoice_status below
1383+ self ._invoice_prevouts_cache = {}
1384+ try :
1385+ for invoice_key in invoice_keys :
1386+ invoice = self ._invoices .get (invoice_key )
1387+ if not invoice :
13911388 continue
1392- is_paid , conf_needed , relevant_txs = self ._is_onchain_invoice_paid (invoice )
1393- if is_paid :
1394- for txid in relevant_txs :
1395- self ._invoices_from_txid_map [txid ].add (invoice_key )
1396- # re-add to the cache, so that self.get_invoice_status below short-circuits
1397- # instead of redoing the prevout scan. not for lightning invoices:
1398- # their LN status takes precedence there (e.g. an in-flight LN payment)
1399- if not invoice .is_lightning () and conf_needed :
1400- self ._paid_invoice_keys_cache .add (invoice_key )
1401- for txout in invoice .get_outputs ():
1402- self ._invoices_from_scriptpubkey_map [txout .scriptpubkey ].add (invoice_key )
1403- # update invoice status
1404- status = self .get_invoice_status (invoice )
1405- util .trigger_callback ('invoice_status' , self , invoice_key , status )
1389+ # clear the cache first so self.get_invoice_status takes the slow path,
1390+ # which is needed to detect paid->unpaid transitions (e.g. reorgs)
1391+ self ._paid_invoice_keys_cache .discard (invoice_key )
1392+ if invoice .is_lightning ():
1393+ is_paid_lightning = bool (self .lnworker and self .lnworker .get_invoice_status (invoice ) == PR_PAID )
1394+ if is_paid_lightning :
1395+ self ._paid_invoice_keys_cache .add (invoice_key )
1396+ if is_paid_lightning or not invoice .get_address ():
1397+ continue
1398+ is_paid , conf_needed , relevant_txs = self ._is_onchain_invoice_paid (invoice )
1399+ if is_paid :
1400+ for txid in relevant_txs :
1401+ self ._invoices_from_txid_map [txid ].add (invoice_key )
1402+ # re-add to the cache, so that self.get_invoice_status below short-circuits
1403+ # instead of redoing the prevout scan. not for lightning invoices:
1404+ # their LN status takes precedence there (e.g. an in-flight LN payment)
1405+ if not invoice .is_lightning () and conf_needed :
1406+ self ._paid_invoice_keys_cache .add (invoice_key )
1407+ for txout in invoice .get_outputs ():
1408+ self ._invoices_from_scriptpubkey_map [txout .scriptpubkey ].add (invoice_key )
1409+ # update invoice status
1410+ status = self .get_invoice_status (invoice )
1411+ util .trigger_callback ('invoice_status' , self , invoice_key , status )
1412+ finally :
1413+ self ._invoice_prevouts_cache = None
14061414
14071415 def _is_onchain_invoice_paid (self , invoice : BaseInvoice ) -> Tuple [bool , Optional [int ], Sequence [str ]]:
14081416 """Returns whether on-chain invoice/request is satisfied, num confs required txs have,
@@ -1419,15 +1427,12 @@ def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional
14191427 conf_needed = None # type: Optional[int]
14201428 with self .lock :
14211429 for invoice_scriptpubkey , invoice_amt in invoice_amounts .items ():
1422- scripthash = bitcoin .script_to_scripthash (invoice_scriptpubkey )
1423- prevouts_and_values = self .db .get_prevouts_by_scripthash (scripthash )
14241430 confs_and_values = []
1425- for prevout , v in prevouts_and_values :
1426- relevant_txs .add (prevout .txid .hex ())
1427- tx_height = self .adb .get_tx_height (prevout .txid .hex ())
1428- if 0 < tx_height .height () <= invoice .height : # exclude txs older than invoice
1431+ for txid , height , conf , v in self ._get_prevout_infos_for_scriptpubkey (invoice_scriptpubkey ):
1432+ relevant_txs .add (txid )
1433+ if 0 < height <= invoice .height : # exclude txs older than invoice
14291434 continue
1430- confs_and_values .append ((tx_height . conf or 0 , v ))
1435+ confs_and_values .append ((conf , v ))
14311436 # check that there is at least one TXO, and that they pay enough.
14321437 # note: "at least one TXO" check is needed for zero amount invoice (e.g. OP_RETURN)
14331438 vsum = 0
@@ -1440,6 +1445,23 @@ def _is_onchain_invoice_paid(self, invoice: BaseInvoice) -> Tuple[bool, Optional
14401445 is_paid = False
14411446 return is_paid , conf_needed , list (relevant_txs )
14421447
1448+ def _get_prevout_infos_for_scriptpubkey (self , scriptpubkey : bytes ) -> Sequence [Tuple [str , int , int , int ]]:
1449+ """Returns (txid, height, conf, value) for each prevout paying scriptpubkey.
1450+ Memoized while an _update_onchain_invoice_paid_detection pass runs (see there).
1451+ """
1452+ cache = self ._invoice_prevouts_cache
1453+ infos = cache .get (scriptpubkey ) if cache is not None else None
1454+ if infos is None :
1455+ scripthash = bitcoin .script_to_scripthash (scriptpubkey )
1456+ infos = []
1457+ for prevout , v in self .db .get_prevouts_by_scripthash (scripthash ):
1458+ txid = prevout .txid .hex ()
1459+ tx_height = self .adb .get_tx_height (txid )
1460+ infos .append ((txid , tx_height .height (), tx_height .conf or 0 , v ))
1461+ if cache is not None :
1462+ cache [scriptpubkey ] = infos
1463+ return infos
1464+
14431465 def is_onchain_invoice_paid (self , invoice : BaseInvoice ) -> Tuple [bool , Optional [int ]]:
14441466 is_paid , conf_needed , relevant_txs = self ._is_onchain_invoice_paid (invoice )
14451467 return is_paid , conf_needed
0 commit comments