Skip to content

Commit 49d9523

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 30d3423 commit 49d9523

File tree

3 files changed

+57
-23
lines changed

3 files changed

+57
-23
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: 47 additions & 17 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,27 @@ 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():
440-
if direction == RECEIVED:
441-
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
442-
# do not redeem this, it might publish the preimage of an incomplete MPP
443-
continue
444-
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
445-
if not preimage:
446-
# we might not have the preimage if this is a hold invoice
447-
continue
448-
else:
449-
preimage = None
449+
preimage = None
450+
if actual_htlc_tx is None: # still in first-stage, so e.g. preimage not revealed yet
451+
if direction == RECEIVED:
452+
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
453+
# - do not redeem this, it might publish the preimage of an incomplete MPP
454+
# - OTOH maybe this chan just got closed, and we are still receiving new htlcs
455+
# for this MPP set. So the MPP set might still transition to complete!
456+
# The MPP_TIMEOUT is only around 2 minutes, so this window is short.
457+
# The default keep_watching logic in lnwatcher is sufficient to call us again.
458+
continue
459+
if htlc.payment_hash in chan.lnworker.dont_settle_htlcs:
460+
prevout = ctx.txid() + ':%d' % ctx_output_idx
461+
txs[prevout] = KeepWatchingTXO(
462+
name=f"our_ctx_htlc_{ctx_output_idx}_for_hold_invoice",
463+
until_height=htlc.cltv_abs,
464+
)
465+
continue
466+
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
467+
if not preimage:
468+
# we might not have the preimage if this is a hold invoice
469+
continue
450470
try:
451471
txs_htlc(
452472
htlc=htlc,
@@ -593,7 +613,7 @@ def sweep_their_ctx_to_remote_backup(
593613

594614
def sweep_their_ctx(
595615
*, chan: 'Channel',
596-
ctx: Transaction) -> Optional[Dict[str, SweepInfo]]:
616+
ctx: Transaction) -> Optional[Dict[str, MaybeSweepInfo]]:
597617
"""Handle the case when the remote force-closes with their ctx.
598618
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
599619
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
@@ -607,7 +627,7 @@ def sweep_their_ctx(
607627
608628
Outputs with CSV/CLTV are redeemed by LNWatcher.
609629
"""
610-
txs = {} # type: Dict[str, SweepInfo]
630+
txs = {} # type: Dict[str, MaybeSweepInfo]
611631
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
612632
x = extract_ctx_secrets(chan, ctx)
613633
if not x:
@@ -737,17 +757,27 @@ def tx_htlc(
737757
subject=REMOTE,
738758
ctn=ctn)
739759
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
760+
preimage = None
740761
is_received_htlc = direction == RECEIVED
741762
if not is_received_htlc and not is_revocation:
742763
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
743-
# do not redeem this, it might publish the preimage of an incomplete MPP
764+
# - do not redeem this, it might publish the preimage of an incomplete MPP
765+
# - OTOH maybe this chan just got closed, and we are still receiving new htlcs
766+
# for this MPP set. So the MPP set might still transition to complete!
767+
# The MPP_TIMEOUT is only around 2 minutes, so this window is short.
768+
# The default keep_watching logic in lnwatcher is sufficient to call us again.
769+
continue
770+
if htlc.payment_hash in chan.lnworker.dont_settle_htlcs:
771+
prevout = ctx.txid() + ':%d' % ctx_output_idx
772+
txs[prevout] = KeepWatchingTXO(
773+
name=f"their_ctx_htlc_{ctx_output_idx}_for_hold_invoice",
774+
until_height=htlc.cltv_abs,
775+
)
744776
continue
745777
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
746778
if not preimage:
747779
# we might not have the preimage if this is a hold invoice
748780
continue
749-
else:
750-
preimage = None
751781
tx_htlc(
752782
htlc=htlc,
753783
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)