From 6598507d3cb1a2a86ec07e7d01998c9fd4bf77b7 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Tue, 10 Dec 2024 13:28:10 +0100 Subject: [PATCH] lnwatcher: replace inspect_tx_candidate with get_spender. inspect_tx_candidate assumes that htlc transactions have only one input, which is not true for anchor channels. inspect_tx_candidate is still used by the watchtower, because it does not have access to channel information. --- electrum/lnwatcher.py | 47 ++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py index b6370ab63..e7e1a185b 100644 --- a/electrum/lnwatcher.py +++ b/electrum/lnwatcher.py @@ -203,18 +203,17 @@ class LNWatcher(Logger, EventListener): # early return if address has not been added yet if not self.adb.is_mine(address): return - spenders = self.inspect_tx_candidate(funding_outpoint, 0) # inspect_tx_candidate might have added new addresses, in which case we return early if not self.adb.is_up_to_date(): return funding_txid = funding_outpoint.split(':')[0] funding_height = self.adb.get_tx_height(funding_txid) - closing_txid = spenders.get(funding_outpoint) + closing_txid = self.get_spender(funding_outpoint) closing_height = self.adb.get_tx_height(closing_txid) if closing_txid: closing_tx = self.adb.get_transaction(closing_txid) if closing_tx: - keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx, spenders) + keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx) else: self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...") keep_watching = True @@ -230,7 +229,7 @@ class LNWatcher(Logger, EventListener): if not keep_watching: await self.unwatch_channel(address, funding_outpoint) - async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spenders) -> bool: + async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool: raise NotImplementedError() # implemented by subclasses async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, @@ -238,6 +237,24 @@ class LNWatcher(Logger, EventListener): closing_height: TxMinedInfo, keep_watching: bool) -> None: raise NotImplementedError() # implemented by subclasses + + def get_spender(self, outpoint) -> str: + """ + returns txid spending outpoint. + subscribes to addresses as a side effect. + """ + prev_txid, index = outpoint.split(':') + spender_txid = self.adb.db.get_spent_outpoint(prev_txid, int(index)) + if not spender_txid: + return + spender_tx = self.adb.get_transaction(spender_txid) + for i, o in enumerate(spender_tx.outputs()): + if o.address is None: + continue + if not self.adb.is_mine(o.address): + self.adb.add_address(o.address) + return spender_txid + def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]: """ returns a dict of spenders for a transaction of interest. @@ -261,6 +278,7 @@ class LNWatcher(Logger, EventListener): spender_tx = self.adb.get_transaction(spender_txid) if n == 1: # if tx input is not a first-stage HTLC, we can stop recursion + # FIXME: this is not true for anchor channels if len(spender_tx.inputs()) != 1: return result o = spender_tx.inputs()[0] @@ -339,7 +357,8 @@ class WatchTower(LNWatcher): for outpoint, address in random_shuffled_copy(lst): self.add_channel(outpoint, address) - async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spenders): + async def sweep_commitment_transaction(self, funding_outpoint, closing_tx): + spenders = self.inspect_tx_candidate(funding_outpoint, 0) keep_watching = False for prevout, spender in spenders.items(): if spender is not None: @@ -436,7 +455,7 @@ class LNWalletWatcher(LNWatcher): await self.lnworker.handle_onchain_state(chan) @log_exceptions - async def sweep_commitment_transaction(self, funding_outpoint, closing_tx, spenders) -> bool: + async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool: """This function is called when a channel was closed. In this case we need to check for redeemable outputs of the commitment transaction or spenders down the line (HTLC-timeout/success transactions). @@ -459,18 +478,18 @@ class LNWalletWatcher(LNWatcher): # do not keep watching if prevout does not exist self.logger.info(f'prevout does not exist for {name}: {prev_txid}') continue - spender_txid = spenders.get(prevout) + spender_txid = self.get_spender(prevout) spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None if spender_tx: # the spender might be the remote, revoked or not htlc_idx_to_sweepinfo = chan.maybe_sweep_revoked_htlcs(closing_tx, spender_tx) for idx, htlc_revocation_sweep_info in htlc_idx_to_sweepinfo.items(): - htlc_tx_spender = spenders.get(spender_txid+f':{idx}') + htlc_tx_spender = self.get_spender(spender_txid+f':{idx}') if htlc_tx_spender: keep_watching |= not self.is_deeply_mined(htlc_tx_spender) else: keep_watching = True - await self.maybe_redeem(spenders, spender_txid+f':{idx}', htlc_revocation_sweep_info, name) + await self.maybe_redeem(spender_txid+f':{idx}', htlc_revocation_sweep_info, name) else: keep_watching |= not self.is_deeply_mined(spender_txid) txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout)) @@ -480,14 +499,14 @@ class LNWalletWatcher(LNWatcher): else: keep_watching = True # broadcast or maybe update our own tx - await self.maybe_redeem(spenders, prevout, sweep_info, name) + await self.maybe_redeem(prevout, sweep_info, name) return keep_watching - def get_redeem_tx(self, spenders, prevout: str, sweep_info: 'SweepInfo', name: str): + def get_redeem_tx(self, prevout: str, sweep_info: 'SweepInfo', name: str): # check if redeem tx needs to be updated # if it is in the mempool, we need to check fee rise - txid = spenders.get(prevout) + txid = self.get_spender(prevout) old_tx = self.adb.get_transaction(txid) assert old_tx is not None or txid is None tx_depth = self.get_tx_mined_depth(txid) if txid else None @@ -517,8 +536,8 @@ class LNWalletWatcher(LNWatcher): assert old_tx is not None return old_tx, None - async def maybe_redeem(self, spenders, prevout, sweep_info: 'SweepInfo', name: str) -> None: - old_tx, new_tx = self.get_redeem_tx(spenders, prevout, sweep_info, name) + async def maybe_redeem(self, prevout, sweep_info: 'SweepInfo', name: str) -> None: + old_tx, new_tx = self.get_redeem_tx(prevout, sweep_info, name) if new_tx is None: return prev_txid, prev_index = prevout.split(':')