lnsweep: lnwatcher needs to keep_watching if htlc in dont_settle_htlcs
If RHASH is in lnworker.dont_settle_htlcs, we should not reveal the preimage. But also, we should not disregard the htlc either. E.g. during a JIT channel open, payment going A->B->C, C would release the preimage to B (lsp) to cover the costs of the JIT channel-open. If the upstream A->B channel gets force-closed, B should only pull the HTLC's funds if he is sure he can forward them to C. lnwatcher needs to keep watching (i.e. wait) until the RHASH gets removed from lnworker.dont_settle_htlcs, or until the CLTV of the HTLC expires.
This commit is contained in:
@@ -55,7 +55,7 @@ from .lnutil import (Outpoint, LocalConfig, RemoteConfig, Keypair, OnlyPubkeyKey
|
|||||||
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
|
received_htlc_trim_threshold_sat, make_commitment_output_to_remote_address, FIXED_ANCHOR_SAT,
|
||||||
ChannelType, LNProtocolWarning, ZEROCONF_TIMEOUT)
|
ChannelType, LNProtocolWarning, ZEROCONF_TIMEOUT)
|
||||||
from .lnsweep import sweep_our_ctx, sweep_their_ctx
|
from .lnsweep import sweep_our_ctx, sweep_their_ctx
|
||||||
from .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo
|
from .lnsweep import sweep_their_htlctx_justice, sweep_our_htlctx, SweepInfo, MaybeSweepInfo
|
||||||
from .lnsweep import sweep_their_ctx_to_remote_backup
|
from .lnsweep import sweep_their_ctx_to_remote_backup
|
||||||
from .lnhtlc import HTLCManager
|
from .lnhtlc import HTLCManager
|
||||||
from .lnmsg import encode_msg, decode_msg
|
from .lnmsg import encode_msg, decode_msg
|
||||||
@@ -286,10 +286,10 @@ class AbstractChannel(Logger, ABC):
|
|||||||
def delete_closing_height(self):
|
def delete_closing_height(self):
|
||||||
self.storage.pop('closing_height', None)
|
self.storage.pop('closing_height', None)
|
||||||
|
|
||||||
def create_sweeptxs_for_our_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
|
def create_sweeptxs_for_our_ctx(self, ctx: Transaction) -> Dict[str, MaybeSweepInfo]:
|
||||||
return sweep_our_ctx(chan=self, ctx=ctx)
|
return sweep_our_ctx(chan=self, ctx=ctx)
|
||||||
|
|
||||||
def create_sweeptxs_for_their_ctx(self, ctx: Transaction) -> Dict[str, SweepInfo]:
|
def create_sweeptxs_for_their_ctx(self, ctx: Transaction) -> Dict[str, MaybeSweepInfo]:
|
||||||
return sweep_their_ctx(chan=self, ctx=ctx)
|
return sweep_their_ctx(chan=self, ctx=ctx)
|
||||||
|
|
||||||
def is_backup(self) -> bool:
|
def is_backup(self) -> bool:
|
||||||
@@ -304,7 +304,7 @@ class AbstractChannel(Logger, ABC):
|
|||||||
def get_remote_peer_sent_error(self) -> Optional[str]:
|
def get_remote_peer_sent_error(self) -> Optional[str]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_ctx_sweep_info(self, ctx: Transaction) -> Tuple[bool, Dict[str, SweepInfo]]:
|
def get_ctx_sweep_info(self, ctx: Transaction) -> Tuple[bool, Dict[str, MaybeSweepInfo]]:
|
||||||
our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx)
|
our_sweep_info = self.create_sweeptxs_for_our_ctx(ctx)
|
||||||
their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx)
|
their_sweep_info = self.create_sweeptxs_for_their_ctx(ctx)
|
||||||
if our_sweep_info:
|
if our_sweep_info:
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ class SweepInfo(NamedTuple):
|
|||||||
return self.txin.get_block_based_relative_locktime() or 0
|
return self.txin.get_block_based_relative_locktime() or 0
|
||||||
|
|
||||||
|
|
||||||
|
class KeepWatchingTXO(NamedTuple):
|
||||||
|
"""Used for UTXOs we don't yet know if we want to sweep, such as pending HTLCs for JIT channels."""
|
||||||
|
name: str
|
||||||
|
until_height: int
|
||||||
|
|
||||||
|
|
||||||
|
MaybeSweepInfo = SweepInfo | KeepWatchingTXO
|
||||||
|
|
||||||
|
|
||||||
def sweep_their_ctx_watchtower(
|
def sweep_their_ctx_watchtower(
|
||||||
chan: 'Channel',
|
chan: 'Channel',
|
||||||
ctx: Transaction,
|
ctx: Transaction,
|
||||||
@@ -282,7 +291,7 @@ def sweep_our_ctx(
|
|||||||
*, chan: 'AbstractChannel',
|
*, chan: 'AbstractChannel',
|
||||||
ctx: Transaction,
|
ctx: Transaction,
|
||||||
actual_htlc_tx: Transaction=None, # if passed, return second stage htlcs
|
actual_htlc_tx: Transaction=None, # if passed, return second stage htlcs
|
||||||
) -> Dict[str, SweepInfo]:
|
) -> Dict[str, MaybeSweepInfo]:
|
||||||
|
|
||||||
"""Handle the case where we force-close unilaterally with our latest ctx.
|
"""Handle the case where we force-close unilaterally with our latest ctx.
|
||||||
|
|
||||||
@@ -329,7 +338,7 @@ def sweep_our_ctx(
|
|||||||
# other outputs are htlcs
|
# other outputs are htlcs
|
||||||
# if they are spent, we need to generate the script
|
# if they are spent, we need to generate the script
|
||||||
# so, second-stage htlc sweep should not be returned here
|
# so, second-stage htlc sweep should not be returned here
|
||||||
txs = {} # type: Dict[str, SweepInfo]
|
txs = {} # type: Dict[str, MaybeSweepInfo]
|
||||||
|
|
||||||
# local anchor
|
# local anchor
|
||||||
if actual_htlc_tx is None and chan.has_anchors():
|
if actual_htlc_tx is None and chan.has_anchors():
|
||||||
@@ -444,9 +453,13 @@ def sweep_our_ctx(
|
|||||||
# note: it is the first stage (witness of htlc_tx) that reveals the preimage,
|
# note: it is the first stage (witness of htlc_tx) that reveals the preimage,
|
||||||
# so if we are already in second stage, it is already revealed.
|
# so if we are already in second stage, it is already revealed.
|
||||||
# However, here, we don't make a distinction.
|
# However, here, we don't make a distinction.
|
||||||
preimage = _maybe_reveal_preimage_for_htlc(
|
preimage, keep_watching_txo = _maybe_reveal_preimage_for_htlc(
|
||||||
chan=chan, htlc=htlc,
|
chan=chan, htlc=htlc,
|
||||||
|
sweep_info_name=f"our_ctx_htlc_{ctx_output_idx}",
|
||||||
)
|
)
|
||||||
|
if keep_watching_txo:
|
||||||
|
prevout = ctx.txid() + ':%d' % ctx_output_idx
|
||||||
|
txs[prevout] = keep_watching_txo
|
||||||
if not preimage:
|
if not preimage:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@@ -465,7 +478,8 @@ def _maybe_reveal_preimage_for_htlc(
|
|||||||
*,
|
*,
|
||||||
chan: 'AbstractChannel',
|
chan: 'AbstractChannel',
|
||||||
htlc: 'UpdateAddHtlc',
|
htlc: 'UpdateAddHtlc',
|
||||||
) -> Optional[bytes]:
|
sweep_info_name: str,
|
||||||
|
) -> Tuple[Optional[bytes], Optional[KeepWatchingTXO]]:
|
||||||
"""Given a Remote-added-HTLC, return the preimage if it's okay to reveal it on-chain."""
|
"""Given a Remote-added-HTLC, return the preimage if it's okay to reveal it on-chain."""
|
||||||
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
|
if not chan.lnworker.is_complete_mpp(htlc.payment_hash):
|
||||||
# - do not redeem this, it might publish the preimage of an incomplete MPP
|
# - do not redeem this, it might publish the preimage of an incomplete MPP
|
||||||
@@ -473,11 +487,16 @@ def _maybe_reveal_preimage_for_htlc(
|
|||||||
# for this MPP set. So the MPP set might still transition to complete!
|
# for this MPP set. So the MPP set might still transition to complete!
|
||||||
# The MPP_TIMEOUT is only around 2 minutes, so this window is short.
|
# The MPP_TIMEOUT is only around 2 minutes, so this window is short.
|
||||||
# The default keep_watching logic in lnwatcher is sufficient to call us again.
|
# The default keep_watching logic in lnwatcher is sufficient to call us again.
|
||||||
return None
|
return None, None
|
||||||
if htlc.payment_hash in chan.lnworker.dont_settle_htlcs:
|
if htlc.payment_hash.hex() in chan.lnworker.dont_settle_htlcs:
|
||||||
return None
|
# we should not reveal the preimage *for now*, but we might still decide to reveal it later
|
||||||
|
keep_watching_txo = KeepWatchingTXO(
|
||||||
|
name=sweep_info_name + "_dont_settle_htlcs",
|
||||||
|
until_height=htlc.cltv_abs,
|
||||||
|
)
|
||||||
|
return None, keep_watching_txo
|
||||||
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
|
preimage = chan.lnworker.get_preimage(htlc.payment_hash)
|
||||||
return preimage
|
return preimage, None
|
||||||
|
|
||||||
|
|
||||||
def extract_ctx_secrets(chan: 'Channel', ctx: Transaction):
|
def extract_ctx_secrets(chan: 'Channel', ctx: Transaction):
|
||||||
@@ -614,7 +633,7 @@ def sweep_their_ctx_to_remote_backup(
|
|||||||
|
|
||||||
def sweep_their_ctx(
|
def sweep_their_ctx(
|
||||||
*, chan: 'Channel',
|
*, chan: 'Channel',
|
||||||
ctx: Transaction) -> Optional[Dict[str, SweepInfo]]:
|
ctx: Transaction) -> Optional[Dict[str, MaybeSweepInfo]]:
|
||||||
"""Handle the case when the remote force-closes with their ctx.
|
"""Handle the case when the remote force-closes with their ctx.
|
||||||
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
|
Sweep outputs that do not have a CSV delay ('to_remote' and first-stage HTLCs).
|
||||||
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
|
Outputs with CSV delay ('to_local' and second-stage HTLCs) are redeemed by LNWatcher.
|
||||||
@@ -628,7 +647,7 @@ def sweep_their_ctx(
|
|||||||
|
|
||||||
Outputs with CSV/CLTV are redeemed by LNWatcher.
|
Outputs with CSV/CLTV are redeemed by LNWatcher.
|
||||||
"""
|
"""
|
||||||
txs = {} # type: Dict[str, SweepInfo]
|
txs = {} # type: Dict[str, MaybeSweepInfo]
|
||||||
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
our_conf, their_conf = get_ordered_channel_configs(chan=chan, for_us=True)
|
||||||
x = extract_ctx_secrets(chan, ctx)
|
x = extract_ctx_secrets(chan, ctx)
|
||||||
if not x:
|
if not x:
|
||||||
@@ -761,9 +780,13 @@ def sweep_their_ctx(
|
|||||||
preimage = None
|
preimage = None
|
||||||
is_received_htlc = direction == RECEIVED
|
is_received_htlc = direction == RECEIVED
|
||||||
if not is_received_htlc and not is_revocation:
|
if not is_received_htlc and not is_revocation:
|
||||||
preimage = _maybe_reveal_preimage_for_htlc(
|
preimage, keep_watching_txo = _maybe_reveal_preimage_for_htlc(
|
||||||
chan=chan, htlc=htlc,
|
chan=chan, htlc=htlc,
|
||||||
|
sweep_info_name=f"their_ctx_htlc_{ctx_output_idx}",
|
||||||
)
|
)
|
||||||
|
if keep_watching_txo:
|
||||||
|
prevout = ctx.txid() + ':%d' % ctx_output_idx
|
||||||
|
txs[prevout] = keep_watching_txo
|
||||||
if not preimage:
|
if not preimage:
|
||||||
continue
|
continue
|
||||||
tx_htlc(
|
tx_htlc(
|
||||||
|
|||||||
@@ -11,11 +11,10 @@ from .transaction import Transaction, TxOutpoint
|
|||||||
from .logging import Logger
|
from .logging import Logger
|
||||||
from .address_synchronizer import TX_HEIGHT_LOCAL
|
from .address_synchronizer import TX_HEIGHT_LOCAL
|
||||||
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY
|
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY
|
||||||
|
from .lnsweep import KeepWatchingTXO, SweepInfo
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .network import Network
|
from .network import Network
|
||||||
from .lnsweep import SweepInfo
|
|
||||||
from .lnworker import LNWallet
|
from .lnworker import LNWallet
|
||||||
from .lnchannel import AbstractChannel
|
from .lnchannel import AbstractChannel
|
||||||
|
|
||||||
@@ -159,6 +158,7 @@ class LNWatcher(Logger, EventListener):
|
|||||||
chan = self.lnworker.channel_by_txo(funding_outpoint)
|
chan = self.lnworker.channel_by_txo(funding_outpoint)
|
||||||
if not chan:
|
if not chan:
|
||||||
return False
|
return False
|
||||||
|
local_height = self.adb.get_local_height()
|
||||||
# detect who closed and get information about how to claim outputs
|
# detect who closed and get information about how to claim outputs
|
||||||
is_local_ctx, sweep_info_dict = chan.get_ctx_sweep_info(closing_tx)
|
is_local_ctx, sweep_info_dict = chan.get_ctx_sweep_info(closing_tx)
|
||||||
# note: we need to keep watching *at least* until the closing tx is deeply mined,
|
# note: we need to keep watching *at least* until the closing tx is deeply mined,
|
||||||
@@ -169,6 +169,10 @@ class LNWatcher(Logger, EventListener):
|
|||||||
prev_txid, prev_index = prevout.split(':')
|
prev_txid, prev_index = prevout.split(':')
|
||||||
name = sweep_info.name + ' ' + chan.get_id_for_log()
|
name = sweep_info.name + ' ' + chan.get_id_for_log()
|
||||||
self.lnworker.wallet.set_default_label(prevout, name)
|
self.lnworker.wallet.set_default_label(prevout, name)
|
||||||
|
if isinstance(sweep_info, KeepWatchingTXO): # haven't yet decided if we want to sweep
|
||||||
|
keep_watching |= sweep_info.until_height > local_height
|
||||||
|
continue
|
||||||
|
assert isinstance(sweep_info, SweepInfo), sweep_info
|
||||||
if not self.adb.get_transaction(prev_txid):
|
if not self.adb.get_transaction(prev_txid):
|
||||||
# do not keep watching if prevout does not exist
|
# do not keep watching if prevout does not exist
|
||||||
self.logger.info(f'prevout does not exist for {name}: {prevout}')
|
self.logger.info(f'prevout does not exist for {name}: {prevout}')
|
||||||
|
|||||||
Reference in New Issue
Block a user