Skip to content

Commit 0787c7c

Browse files
committed
lnsweep: lnwatcher needs to keep_waiting for pending hold_invoice
If RHASH is in lnworker.dont_settle_htlcs, we should not reveal the preimage. But also, we should not disregard the htlc either. lnwatcher needs to keep waiting until either the user cancels/settles the hold invoice, or until the hold_invoice's CLTV expires.
1 parent 5302606 commit 0787c7c

File tree

3 files changed

+52
-16
lines changed

3 files changed

+52
-16
lines changed

electrum/lnchannel.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
5656
ChannelType, LNProtocolWarning, ZEROCONF_TIMEOUT)
5757
from .lnsweep import sweep_our_ctx, sweep_their_ctx
58-
from .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo
58+
from .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo, MaybeSweepInfo
5959
from .lnsweep import sweep_their_ctx_to_remote_backup
6060
from .lnhtlc import HTLCManager
6161
from .lnmsg import encode_msg, decode_msg
@@ -286,10 +286,10 @@ def get_closing_height(self) -> Optional[Tuple[str, int, Optional[int]]]:
286286
def delete_closing_height(self):
287287
self.storage.pop('closing_height', None)
288288

289-
def create_sweeptxs_for_our_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
289+
def create_sweeptxs_for_our_ctx(self, ctx: Transaction) -> Dict[str, MaybeSweepInfo]:
290290
return sweep_our_ctx(chan=self, ctx=ctx)
291291

292-
def create_sweeptxs_for_their_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
292+
def create_sweeptxs_for_their_ctx(self, ctx: Transaction) -> Dict[str, MaybeSweepInfo]:
293293
return sweep_their_ctx(chan=self, ctx=ctx)
294294

295295
def is_backup(self) -> bool:
@@ -304,7 +304,7 @@ def get_remote_scid_alias(self) -> Optional[bytes]:
304304
def get_remote_peer_sent_error(self) -> Optional[str]:
305305
return None
306306

307-
def get_ctx_sweep_info(self, ctx: Transaction) -> Tuple[bool, Dict[str, SweepInfo]]:
307+
def get_ctx_sweep_info(self, ctx: Transaction) -> Tuple[bool, Dict[str, MaybeSweepInfo]]:
308308
our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx)
309309
their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx)
310310
if our_sweep_info:

electrum/lnsweep.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ def csv_delay(self):
5353
return self.txin.get_block_based_relative_locktime() or 0
5454

5555

56+
class KeepWatchingTXO(NamedTuple):
57+
"""Used for UTXOs we don't yet know if we want to sweep, such as pending hold-invoices."""
58+
name: str
59+
until_height: int
60+
61+
62+
MaybeSweepInfo = SweepInfo | KeepWatchingTXO
63+
64+
5665
def sweep_their_ctx_watchtower(
5766
chan: 'Channel',
5867
ctx: Transaction,
@@ -281,7 +290,7 @@ def sweep_our_ctx(
281290
*, chan: 'AbstractChannel',
282291
ctx: Transaction,
283292
actual_htlc_tx: Transaction=None, # if passed, return second stage htlcs
284-
) -> Dict[str, SweepInfo]:
293+
) -> Dict[str, MaybeSweepInfo]:
285294

286295
"""Handle the case where we force-close unilaterally with our latest ctx.
287296
@@ -328,7 +337,7 @@ def sweep_our_ctx(
328337
# other outputs are htlcs
329338
# if they are spent, we need to generate the script
330339
# so, second-stage htlc sweep should not be returned here
331-
txs = {} # type: Dict[str, SweepInfo]
340+
txs = {} # type: Dict[str, MaybeSweepInfo]
332341

333342
# local anchor
334343
if actual_htlc_tx is None and chan.has_anchors():
@@ -437,16 +446,29 @@ def txs_htlc(
437446
subject=LOCAL,
438447
ctn=ctn)
439448
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
449+
preimage = None
440450
if direction == RECEIVED:
451+
# note: it is the first stage (witness of htlc_tx) that reveals the preimage,
452+
# so if we are already in second stage, it is already revealed.
453+
# However, here, we don't make a distinction.
441454
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
442-
# do not redeem this, it might publish the preimage of an incomplete MPP
455+
# - do not redeem this, it might publish the preimage of an incomplete MPP
456+
# - OTOH maybe this chan just got closed, and we are still receiving new htlcs
457+
# for this MPP set. So the MPP set might still transition to complete!
458+
# The MPP_TIMEOUT is only around 2 minutes, so this window is short.
459+
# The default keep_watching logic in lnwatcher is sufficient to call us again.
460+
continue
461+
if htlc.payment_hash in chan.lnworker.dont_settle_htlcs:
462+
prevout = ctx.txid() + ':%d' % ctx_output_idx
463+
txs[prevout] = KeepWatchingTXO(
464+
name=f"our_ctx_htlc_{ctx_output_idx}_for_hold_invoice",
465+
until_height=htlc.cltv_abs,
466+
)
443467
continue
444468
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
445469
if not preimage:
446470
# we might not have the preimage if this is a hold invoice
447471
continue
448-
else:
449-
preimage = None
450472
try:
451473
txs_htlc(
452474
htlc=htlc,
@@ -593,7 +615,7 @@ def sweep_their_ctx_to_remote_backup(
593615

594616
def sweep_their_ctx(
595617
*, chan: 'Channel',
596-
ctx: Transaction) -> Optional[Dict[str, SweepInfo]]:
618+
ctx: Transaction) -> Optional[Dict[str, MaybeSweepInfo]]:
597619
"""Handle the case when the remote force-closes with their ctx.
598620
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
599621
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
@@ -607,7 +629,7 @@ def sweep_their_ctx(
607629
608630
Outputs with CSV/CLTV are redeemed by LNWatcher.
609631
"""
610-
txs = {} # type: Dict[str, SweepInfo]
632+
txs = {} # type: Dict[str, MaybeSweepInfo]
611633
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
612634
x = extract_ctx_secrets(chan, ctx)
613635
if not x:
@@ -737,17 +759,27 @@ def tx_htlc(
737759
subject=REMOTE,
738760
ctn=ctn)
739761
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
762+
preimage = None
740763
is_received_htlc = direction == RECEIVED
741764
if not is_received_htlc and not is_revocation:
742765
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
743-
# do not redeem this, it might publish the preimage of an incomplete MPP
766+
# - do not redeem this, it might publish the preimage of an incomplete MPP
767+
# - OTOH maybe this chan just got closed, and we are still receiving new htlcs
768+
# for this MPP set. So the MPP set might still transition to complete!
769+
# The MPP_TIMEOUT is only around 2 minutes, so this window is short.
770+
# The default keep_watching logic in lnwatcher is sufficient to call us again.
771+
continue
772+
if htlc.payment_hash in chan.lnworker.dont_settle_htlcs:
773+
prevout = ctx.txid() + ':%d' % ctx_output_idx
774+
txs[prevout] = KeepWatchingTXO(
775+
name=f"their_ctx_htlc_{ctx_output_idx}_for_hold_invoice",
776+
until_height=htlc.cltv_abs,
777+
)
744778
continue
745779
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
746780
if not preimage:
747781
# we might not have the preimage if this is a hold invoice
748782
continue
749-
else:
750-
preimage = None
751783
tx_htlc(
752784
htlc=htlc,
753785
is_received_htlc=is_received_htlc,

electrum/lnwatcher.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@
1111
from .logging import Logger
1212
from .address_synchronizer import TX_HEIGHT_LOCAL
1313
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY
14-
14+
from .lnsweep import KeepWatchingTXO, SweepInfo
1515

1616
if TYPE_CHECKING:
1717
from .network import Network
18-
from .lnsweep import SweepInfo
1918
from .lnworker import LNWallet
2019
from .lnchannel import AbstractChannel
2120

@@ -160,6 +159,7 @@ async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx:
160159
if not chan.need_to_subscribe():
161160
return False
162161
self.logger.info(f'sweep_commitment_transaction {funding_outpoint}')
162+
local_height = self.adb.get_local_height()
163163
# detect who closed and get information about how to claim outputs
164164
is_local_ctx, sweep_info_dict = chan.get_ctx_sweep_info(closing_tx)
165165
# note: we need to keep watching *at least* until the closing tx is deeply mined,
@@ -170,6 +170,10 @@ async def sweep_commitment_transaction(self, funding_outpoint: str, closing_tx:
170170
prev_txid, prev_index = prevout.split(':')
171171
name = sweep_info.name + ' ' + chan.get_id_for_log()
172172
self.lnworker.wallet.set_default_label(prevout, name)
173+
if isinstance(sweep_info, KeepWatchingTXO): # haven't yet decided if we want to sweep
174+
keep_watching |= sweep_info.until_height > local_height
175+
continue
176+
assert isinstance(sweep_info, SweepInfo), sweep_info
173177
if not self.adb.get_transaction(prev_txid):
174178
# do not keep watching if prevout does not exist
175179
self.logger.info(f'prevout does not exist for {name}: {prevout}')

0 commit comments

Comments
 (0)