What changed, and why it matters
This commit is a small follow-up to a previous change in Electrum's Lightning watchtower code. It changes how the wallet decides whether to keep monitoring certain transactions, allowing the code to say 'keep watching but don't sweep yet' instead of always requiring an immediate sweep action. The commit itself does not appear to fix a clear exploit; it is more likely a correctness or robustness improvement to avoid premature decisions.
Review the preceding commit ('prev') to understand the full context. Treat this as a defensive hardening change rather than an urgent security fix unless additional information shows otherwise. Continue normal testing of Lightning channel closure and watchtower behavior.
Security signals we found
Changes Lightning watchtower/HTLC sweeping logic
Introduces a 'keep watching without sweeping' branch
Moves label assignment before sweep decision
Adds an assert to enforce SweepInfo type after filtering KeepWatchingTXO
Evidence from the diff
The patch updates lnchannel.py so that maybe_sweep_htlcs() can return a Dict[str, MaybeSweepInfo] rather than Dict[str, SweepInfo], where MaybeSweepInfo is a union that includes a new KeepWatchingTXO sentinel. In lnwatcher.py, the loop over HTLC sweep info now checks for KeepWatchingTXO first: if encountered, it extends the watching window based on until_height and skips sweeping. Otherwise it asserts the value is a SweepInfo and proceeds with the existing sweep logic. The label assignment is moved earlier. This is a follow-up to an earlier commit (referenced as ‘prev’) and appears to refine state handling for HTLC outputs that are not yet ready to be swept.
Changed components
electrum/lnchannel.pyelectrum/lnwatcher.pyLightning HTLC sweep/watchtower logicInspect captured patch +8 / −4
diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py
index df34e8c..683f44a 100644
--- a/electrum/lnchannel.py
+++ b/electrum/lnchannel.py
@@ -327,7 +327,7 @@ class AbstractChannel(Logger, ABC):
is_local_ctx = who_closed == LOCAL
return is_local_ctx, sweep_info
- def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, SweepInfo]:
+ def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, MaybeSweepInfo]:
return {}
def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:
@@ -682,7 +682,7 @@ class ChannelBackup(AbstractChannel):
else:
return {}
- def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, SweepInfo]:
+ def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, MaybeSweepInfo]:
return {}
def extract_preimage_from_htlc_txin(self, txin: TxInput, *, is_deeply_mined: bool) -> None:
@@ -1939,7 +1939,7 @@ class Channel(AbstractChannel):
assert not (self.get_state() == ChannelState.WE_ARE_TOXIC and ChanCloseOption.LOCAL_FCLOSE in ret), "local force-close unsafe if we are toxic"
return ret
- def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, SweepInfo]:
+ def maybe_sweep_htlcs(self, ctx: Transaction, htlc_tx: Transaction) -> Dict[str, MaybeSweepInfo]:
# look at the output address, check if it matches
d = sweep_their_htlctx_justice(self, ctx, htlc_tx)
d2 = sweep_our_htlctx(self, ctx, htlc_tx)
diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
index 354b0a6..4db9b04 100644
--- a/electrum/lnwatcher.py
+++ b/electrum/lnwatcher.py
@@ -184,9 +184,13 @@ class LNWatcher(Logger, EventListener):
# the spender might be the remote, revoked or not
htlc_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx)
for prevout2, htlc_sweep_info in htlc_sweepinfo.items():
+ self.lnworker.wallet.set_default_label(prevout2, htlc_sweep_info.name)
+ if isinstance(htlc_sweep_info, KeepWatchingTXO): # haven't yet decided if we want to sweep
+ keep_watching |= htlc_sweep_info.until_height > local_height
+ continue
+ assert isinstance(htlc_sweep_info, SweepInfo), htlc_sweep_info
watch_htlc_sweep_info = self.maybe_redeem(htlc_sweep_info)
htlc_tx_spender = self.adb.get_spender(prevout2)
- self.lnworker.wallet.set_default_label(prevout2, htlc_sweep_info.name)
if htlc_tx_spender:
keep_watching |= not self.adb.is_deeply_mined(htlc_tx_spender)
self.maybe_add_accounting_address(htlc_tx_spender, htlc_sweep_info)
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.