1
0

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.
This commit is contained in:
ThomasV
2024-12-10 13:28:10 +01:00
parent 26ed696479
commit 6598507d3c

View File

@@ -203,18 +203,17 @@ class LNWatcher(Logger, EventListener):
# early return if address has not been added yet # early return if address has not been added yet
if not self.adb.is_mine(address): if not self.adb.is_mine(address):
return return
spenders = self.inspect_tx_candidate(funding_outpoint, 0)
# inspect_tx_candidate might have added new addresses, in which case we return early # inspect_tx_candidate might have added new addresses, in which case we return early
if not self.adb.is_up_to_date(): if not self.adb.is_up_to_date():
return return
funding_txid = funding_outpoint.split(':')[0] funding_txid = funding_outpoint.split(':')[0]
funding_height = self.adb.get_tx_height(funding_txid) 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) closing_height = self.adb.get_tx_height(closing_txid)
if closing_txid: if closing_txid:
closing_tx = self.adb.get_transaction(closing_txid) closing_tx = self.adb.get_transaction(closing_txid)
if closing_tx: 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: else:
self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...") self.logger.info(f"channel {funding_outpoint} closed by {closing_txid}. still waiting for tx itself...")
keep_watching = True keep_watching = True
@@ -230,7 +229,7 @@ class LNWatcher(Logger, EventListener):
if not keep_watching: if not keep_watching:
await self.unwatch_channel(address, funding_outpoint) 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 raise NotImplementedError() # implemented by subclasses
async def update_channel_state(self, *, funding_outpoint: str, funding_txid: str, 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: closing_height: TxMinedInfo, keep_watching: bool) -> None:
raise NotImplementedError() # implemented by subclasses 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]: def inspect_tx_candidate(self, outpoint, n: int) -> Dict[str, str]:
""" """
returns a dict of spenders for a transaction of interest. 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) spender_tx = self.adb.get_transaction(spender_txid)
if n == 1: if n == 1:
# if tx input is not a first-stage HTLC, we can stop recursion # 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: if len(spender_tx.inputs()) != 1:
return result return result
o = spender_tx.inputs()[0] o = spender_tx.inputs()[0]
@@ -339,7 +357,8 @@ class WatchTower(LNWatcher):
for outpoint, address in random_shuffled_copy(lst): for outpoint, address in random_shuffled_copy(lst):
self.add_channel(outpoint, address) 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 keep_watching = False
for prevout, spender in spenders.items(): for prevout, spender in spenders.items():
if spender is not None: if spender is not None:
@@ -436,7 +455,7 @@ class LNWalletWatcher(LNWatcher):
await self.lnworker.handle_onchain_state(chan) await self.lnworker.handle_onchain_state(chan)
@log_exceptions @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 """This function is called when a channel was closed. In this case
we need to check for redeemable outputs of the commitment transaction we need to check for redeemable outputs of the commitment transaction
or spenders down the line (HTLC-timeout/success transactions). 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 # do not keep watching if prevout does not exist
self.logger.info(f'prevout does not exist for {name}: {prev_txid}') self.logger.info(f'prevout does not exist for {name}: {prev_txid}')
continue 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 spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None
if spender_tx: if spender_tx:
# the spender might be the remote, revoked or not # the spender might be the remote, revoked or not
htlc_idx_to_sweepinfo = chan.maybe_sweep_revoked_htlcs(closing_tx, spender_tx) 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(): 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: if htlc_tx_spender:
keep_watching |= not self.is_deeply_mined(htlc_tx_spender) keep_watching |= not self.is_deeply_mined(htlc_tx_spender)
else: else:
keep_watching = True 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: else:
keep_watching |= not self.is_deeply_mined(spender_txid) keep_watching |= not self.is_deeply_mined(spender_txid)
txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout)) txin_idx = spender_tx.get_input_idx_that_spent_prevout(TxOutpoint.from_str(prevout))
@@ -480,14 +499,14 @@ class LNWalletWatcher(LNWatcher):
else: else:
keep_watching = True keep_watching = True
# broadcast or maybe update our own tx # 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 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 # check if redeem tx needs to be updated
# if it is in the mempool, we need to check fee rise # 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) old_tx = self.adb.get_transaction(txid)
assert old_tx is not None or txid is None assert old_tx is not None or txid is None
tx_depth = self.get_tx_mined_depth(txid) if txid else 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 assert old_tx is not None
return old_tx, None return old_tx, None
async def maybe_redeem(self, spenders, prevout, sweep_info: 'SweepInfo', name: str) -> None: async def maybe_redeem(self, prevout, sweep_info: 'SweepInfo', name: str) -> None:
old_tx, new_tx = self.get_redeem_tx(spenders, prevout, sweep_info, name) old_tx, new_tx = self.get_redeem_tx(prevout, sweep_info, name)
if new_tx is None: if new_tx is None:
return return
prev_txid, prev_index = prevout.split(':') prev_txid, prev_index = prevout.split(':')