adb.get_spender: subscribe to outputs explicitly, instead of as a side effect
What changed, and why it matters
This commit refactors how Electrum subscribes to Bitcoin addresses when watching Lightning Network channel closes. Previously, checking which transaction spent a channel funding output automatically subscribed to related addresses. Now that subscription is done explicitly and only when actually needed. The change is a defensive cleanup that reduces unnecessary network subscriptions and could prevent a subtle bug where the wallet fails to track important transactions in some edge cases.
Review and merge if part of normal maintenance. Monitor for any regressions in Lightning channel monitoring or watchtower functionality, as the change moves subscription responsibility to callers.
Security signals we found
Side-effect removal in address subscription logic
Lightning channel closing transaction tracking
Watchtower plugin address subscription behavior
HTLC sweep output subscription
Evidence from the diff
The patch splits AddressSynchronizer.get_spender() into two methods: get_spender() now only returns the spending txid without side effects, and subscribe_to_outputs() handles address subscription for the spending transaction’s outputs. Callers in LNWatcher and the watchtower plugin now explicitly call subscribe_to_outputs() only when they have a valid closing transaction or HTLC sweep information. This makes subscriptions intentional rather than an implicit side effect of querying the spender.
Changed components
electrum/address_synchronizer.pyelectrum/lnwatcher.pyelectrum/plugins/watchtower/watchtower.pyInspect captured patch +7 / −4
diff --git a/electrum/address_synchronizer.py b/electrum/address_synchronizer.py
index 9d061dd..3e9502e 100644
--- a/electrum/address_synchronizer.py
+++ b/electrum/address_synchronizer.py
@@ -1047,7 +1047,6 @@ class AddressSynchronizer(Logger, EventListener):
def get_spender(self, outpoint: str) -> Optional[str]:
"""
returns txid spending outpoint.
- subscribes to addresses as a side effect.
"""
prev_txid, index = outpoint.split(':')
spender_txid = self.db.get_spent_outpoint(prev_txid, int(index))
@@ -1055,15 +1054,15 @@ class AddressSynchronizer(Logger, EventListener):
tx_mined_status = self.get_tx_height(spender_txid)
if tx_mined_status.height() in [TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE]:
spender_txid = None
- if not spender_txid:
- return None
+ return spender_txid
+
+ def subscribe_to_outputs(self, spender_txid: str):
spender_tx = self.get_transaction(spender_txid)
for i, o in enumerate(spender_tx.outputs()):
if o.address is None:
continue
if not self.is_mine(o.address):
self.add_address(o.address)
- return spender_txid
def get_tx_mined_depth(self, txid: str):
if not txid:
diff --git a/electrum/lnwatcher.py b/electrum/lnwatcher.py
index 75ae633..2a9d0fa 100644
--- a/electrum/lnwatcher.py
+++ b/electrum/lnwatcher.py
@@ -111,6 +111,7 @@ class LNWatcher(Logger, EventListener):
closing_txid = self.adb.get_spender(funding_outpoint)
closing_height = self.adb.get_tx_height(closing_txid)
if closing_txid:
+ self.adb.subscribe_to_outputs(closing_txid)
closing_tx = self.adb.get_transaction(closing_txid)
if closing_tx:
keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx)
@@ -189,6 +190,8 @@ class LNWatcher(Logger, EventListener):
if spender_tx:
# the spender might be the remote, revoked or not
htlc_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx)
+ if htlc_sweepinfo:
+ self.adb.subscribe_to_outputs(spender_txid)
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
diff --git a/electrum/plugins/watchtower/watchtower.py b/electrum/plugins/watchtower/watchtower.py
index 04212c2..4b16130 100644
--- a/electrum/plugins/watchtower/watchtower.py
+++ b/electrum/plugins/watchtower/watchtower.py
@@ -140,6 +140,7 @@ class WatchTower(Logger, EventListener):
# inspect_tx_candidate might have added new addresses, in which case we return early
closing_txid = self.adb.get_spender(funding_outpoint)
if closing_txid:
+ self.adb.subscribe_to_outputs(closing_txid)
closing_tx = self.adb.get_transaction(closing_txid)
if closing_tx:
keep_watching = await self.sweep_commitment_transaction(funding_outpoint, closing_tx)
Why this scored 25/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.