Skip to content

Commit ac64064

Browse files
committed
lnwatcher: keep watching sweep TXOs that are dust due to high fees
- if fee estimates are high atm, some outputs are not worth to sweep - however, fee estimates might be only-temporarily very high - previously in such a case lnwatcher would just discard outputs as dust, and mark the channel REDEEMED (and hence never watch it or try again) - now, instead, if the outputs would not be dust if fee estimates were lower, lnwatcher will keep watching the channel - and if estimates go down, lnwatcher will sweep them then - relatedly, previously txbatcher.is_dust() used allow_fallback_to_static_rates=True, and it erroneously almost always fell back to the static rates (150 s/b) during startup (race: lnwatcher was faster than the network managed to get estimates) - now, instead, txbatcher.is_dust() does not fallback to static rates, and the callers are supposed to handle NoDynamicFeeEstimates. - I think this makes much more sense. The previous meaning of "is_dust" with the fallback was weird. Now it means: "is dust at current feerates". fixes #9980
1 parent 9d12a69 commit ac64064

File tree

3 files changed

+26
-12
lines changed

3 files changed

+26
-12
lines changed

electrum/lnwatcher.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import TYPE_CHECKING, Optional
66

77
from . import util
8-
from .util import TxMinedInfo, BelowDustLimit
8+
from .util import TxMinedInfo, BelowDustLimit, NoDynamicFeeEstimates
99
from .util import EventListener, event_listener, log_exceptions, ignore_exceptions
1010
from .transaction import Transaction, TxOutpoint
1111
from .logging import Logger
@@ -172,38 +172,44 @@ async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx:
172172
# do not keep watching if prevout does not exist
173173
self.logger.info(f'prevout does not exist for {name}: {prevout}')
174174
continue
175-
was_added = self.maybe_redeem(sweep_info)
175+
watch_sweep_info = self.maybe_redeem(sweep_info)
176176
spender_txid = self.adb.get_spender(prevout) # note: LOCAL spenders don't count
177177
spender_tx = self.adb.get_transaction(spender_txid) if spender_txid else None
178178
if spender_tx:
179179
# the spender might be the remote, revoked or not
180180
htlc_sweepinfo = chan.maybe_sweep_htlcs(closing_tx, spender_tx)
181181
for prevout2, htlc_sweep_info in htlc_sweepinfo.items():
182-
htlc_was_added = self.maybe_redeem(htlc_sweep_info)
182+
watch_htlc_sweep_info = self.maybe_redeem(htlc_sweep_info)
183183
htlc_tx_spender = self.adb.get_spender(prevout2)
184184
self.lnworker.wallet.set_default_label(prevout2, htlc_sweep_info.name)
185185
if htlc_tx_spender:
186186
keep_watching |= not self.adb.is_deeply_mined(htlc_tx_spender)
187187
self.maybe_add_accounting_address(htlc_tx_spender, htlc_sweep_info)
188188
else:
189-
keep_watching |= htlc_was_added
189+
keep_watching |= watch_htlc_sweep_info
190190
keep_watching |= not self.adb.is_deeply_mined(spender_txid)
191191
self.maybe_extract_preimage(chan, spender_tx, prevout)
192192
self.maybe_add_accounting_address(spender_txid, sweep_info)
193193
else:
194-
keep_watching |= was_added
194+
keep_watching |= watch_sweep_info
195195
self.maybe_add_pending_forceclose(
196-
chan=chan, spender_txid=spender_txid, is_local_ctx=is_local_ctx, sweep_info=sweep_info, was_added=was_added)
196+
chan=chan, spender_txid=spender_txid, is_local_ctx=is_local_ctx, sweep_info=sweep_info)
197197
return keep_watching
198198

199199
def get_pending_force_closes(self):
200200
return self._pending_force_closes
201201

202202
def maybe_redeem(self, sweep_info: 'SweepInfo') -> bool:
203-
""" returns False if it was dust """
203+
""" returns 'keep_watching' """
204204
try:
205205
self.lnworker.wallet.txbatcher.add_sweep_input('lnwatcher', sweep_info)
206206
except BelowDustLimit:
207+
# utxo is considered dust at *current* fee estimates.
208+
# but maybe the fees atm are very high? We will retry later.
209+
pass
210+
except NoDynamicFeeEstimates:
211+
pass # will retry later
212+
if sweep_info.is_anchor():
207213
return False
208214
return True
209215

@@ -254,10 +260,9 @@ def maybe_add_pending_forceclose(
254260
spender_txid: Optional[str],
255261
is_local_ctx: bool,
256262
sweep_info: 'SweepInfo',
257-
was_added: bool,
258263
):
259264
""" we are waiting for ctx to be confirmed and there are received htlcs """
260-
if was_added and is_local_ctx and sweep_info.name == 'received-htlc' and chan.has_anchors():
265+
if is_local_ctx and sweep_info.name == 'received-htlc' and chan.has_anchors():
261266
tx_mined_status = self.adb.get_tx_height(spender_txid)
262267
if tx_mined_status.height == TX_HEIGHT_LOCAL:
263268
self._pending_force_closes.add(chan)

electrum/submarine_swaps.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from .util import (
3333
log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, ca_path, gen_nostr_ann_pow,
3434
get_nostr_ann_pow_amount, make_aiohttp_proxy_connector, get_running_loop, get_asyncio_loop, wait_for2,
35-
run_sync_function_on_asyncio_thread, trigger_callback
35+
run_sync_function_on_asyncio_thread, trigger_callback, NoDynamicFeeEstimates
3636
)
3737
from . import lnutil
3838
from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
@@ -485,6 +485,9 @@ async def _claim_swap(self, swap: SwapData) -> None:
485485
except BelowDustLimit:
486486
self.logger.info('utxo value below dust threshold')
487487
return
488+
except NoDynamicFeeEstimates:
489+
self.logger.info('got NoDynamicFeeEstimates')
490+
return
488491

489492
def get_fee_for_txbatcher(self):
490493
return self._get_tx_fee(self.config.FEE_POLICY_SWAPS)

electrum/txbatcher.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def add_payment_output(self, key: str, output: 'PartialTxOutput') -> None:
9999

100100
@locked
101101
def add_sweep_input(self, key: str, sweep_info: 'SweepInfo') -> None:
102+
"""Can raise BelowDustLimit or NoDynamicFeeEstimates."""
102103
if sweep_info.txin and sweep_info.txout:
103104
# detect legacy htlc using name and csv delay
104105
if sweep_info.name in ['received-htlc', 'offered-htlc'] and sweep_info.csv_delay == 0:
@@ -263,20 +264,25 @@ def add_payment_output(self, output: 'PartialTxOutput') -> None:
263264
self.batch_payments.append(output)
264265

265266
def is_dust(self, sweep_info: SweepInfo) -> bool:
267+
"""Can raise BelowDustLimit or NoDynamicFeeEstimates."""
266268
if sweep_info.is_anchor():
267269
return False
268270
if sweep_info.txout is not None:
269271
return False
270-
value = sweep_info.txin._trusted_value_sats
272+
value = sweep_info.txin.value_sats()
271273
witness_size = len(sweep_info.txin.make_witness(71*b'\x00'))
272274
tx_size_vbytes = 84 + witness_size//4 # assumes no batching, sweep to p2wpkh
273275
self.logger.info(f'{sweep_info.name} size = {tx_size_vbytes}')
274-
fee = self.fee_policy.estimate_fee(tx_size_vbytes, network=self.wallet.network, allow_fallback_to_static_rates=True)
276+
fee = self.fee_policy.estimate_fee(tx_size_vbytes, network=self.wallet.network)
275277
return value - fee <= dust_threshold()
276278

277279
@locked
278280
def add_sweep_input(self, sweep_info: 'SweepInfo') -> None:
281+
"""Can raise BelowDustLimit or NoDynamicFeeEstimates."""
279282
if self.is_dust(sweep_info):
283+
# note: this uses the current fee estimates. Just because something is dust
284+
# at the current fee levels, if fees go down, it might still become
285+
# worthwhile to sweep. So callers might want to retry later.
280286
raise BelowDustLimit
281287
txin = sweep_info.txin
282288
if txin.prevout in self._unconfirmed_sweeps:

0 commit comments

Comments
 (0)