tlnworker: rework "is_dangerous" - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 0973b869251895fb796d98afcb877adad4c327b8 DIR parent ce54b5411e78e04f54169c01a3ba94e8bdca876d HTML Author: SomberNight <somber.night@protonmail.com> Date: Wed, 14 Aug 2019 21:38:02 +0200 lnworker: rework "is_dangerous" "Should channel be closed due to expiring htlcs?" Diffstat: M electrum/lnchannel.py | 4 ---- M electrum/lnhtlc.py | 6 ++++++ M electrum/lnutil.py | 25 +++++++++++++++++++++++-- M electrum/lnworker.py | 45 +++++++++++++++++++++++-------- 4 files changed, 63 insertions(+), 17 deletions(-) --- DIR diff --git a/electrum/lnchannel.py b/electrum/lnchannel.py t@@ -551,10 +551,6 @@ class Channel(Logger): assert type(direction) is Direction return htlcsum(self.hm.all_settled_htlcs_ever_by_direction(LOCAL, direction)) - def get_unfulfilled_htlcs(self): - log = self.hm.log[REMOTE] - return [v for x,v in log['adds'].items() if x not in log['settles']] - def settle_htlc(self, preimage, htlc_id): """ SettleHTLC attempts to settle an existing outstanding received HTLC. DIR diff --git a/electrum/lnhtlc.py b/electrum/lnhtlc.py t@@ -271,6 +271,12 @@ class HTLCManager: ctn = self.ctn_latest(subject) + 1 return self.htlcs(subject, ctn) + def was_htlc_preimage_released(self, *, htlc_id: int, htlc_sender: HTLCOwner) -> bool: + settles = self.log[htlc_sender]['settles'] + if htlc_id not in settles: + return False + return settles[htlc_id][htlc_sender] is not None + def all_settled_htlcs_ever_by_direction(self, subject: HTLCOwner, direction: Direction, ctn: int = None) -> Sequence[UpdateAddHtlc]: """Return the list of all HTLCs that have been ever settled in subject's DIR diff --git a/electrum/lnutil.py b/electrum/lnutil.py t@@ -114,10 +114,31 @@ class RemoteMisbehaving(LightningError): pass class NotFoundChanAnnouncementForUpdate(Exception): pass -# TODO make configurable? +# TODO make some of these values configurable? DEFAULT_TO_SELF_DELAY = 144 + + +##### CLTV-expiry-delta-related values +# see https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection + +# the minimum cltv_expiry accepted for terminal payments MIN_FINAL_CLTV_EXPIRY_ACCEPTED = 144 -MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE = MIN_FINAL_CLTV_EXPIRY_ACCEPTED + 1 +# set it a tiny bit higher for invoices as blocks could get mined +# during forward path of payment +MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE = MIN_FINAL_CLTV_EXPIRY_ACCEPTED + 3 + +# the deadline for offered HTLCs: +# the deadline after which the channel has to be failed and timed out on-chain +NBLOCK_DEADLINE_AFTER_EXPIRY_FOR_OFFERED_HTLCS = 1 + +# the deadline for received HTLCs this node has fulfilled: +# the deadline after which the channel has to be failed and the HTLC fulfilled on-chain before its cltv_expiry +NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS = 72 + +# the cltv_expiry_delta for channels when we are forwarding payments +NBLOCK_OUR_CLTV_EXPIRY_DELTA = 144 + +NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE = 4032 # When we open a channel, the remote peer has to support at least this DIR diff --git a/electrum/lnworker.py b/electrum/lnworker.py t@@ -36,6 +36,7 @@ from .lnpeer import Peer from .lnaddr import lnencode, LnAddr, lndecode from .ecc import der_sig_from_sig_string from .lnchannel import Channel, ChannelJsonEncoder +from . import lnutil from .lnutil import (Outpoint, calc_short_channel_id, LNPeerAddr, get_compressed_pubkey_from_bech32, extract_nodeid, PaymentFailure, split_host_port, ConnStringFormatError, t@@ -628,16 +629,38 @@ class LNWallet(LNWorker): except Exception as e: self.logger.info(f'could not add future tx: {name}. prevout: {prevout} {str(e)}') - def is_dangerous(self, chan): - for x in chan.get_unfulfilled_htlcs(): - dust_limit = chan.config[REMOTE].dust_limit_sat * 1000 - delay = x.cltv_expiry - self.network.get_local_height() - if x.amount_msat > 10 * dust_limit and delay < 3: - self.logger.info('htlc is dangerous') - return True - else: - self.logger.info(f'htlc is not dangerous. delay {delay}') - return False + def should_channel_be_closed_due_to_expiring_htlcs(self, chan: Channel) -> bool: + local_height = self.network.get_local_height() + htlcs_we_could_reclaim = {} # type: Dict[Tuple[Direction, int], UpdateAddHtlc] + # If there is a received HTLC for which we already released the preimage + # but the remote did not revoke yet, and the CLTV of this HTLC is dangerously close + # to the present, then unilaterally close channel + recv_htlc_deadline = lnutil.NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS + for sub, dir, ctn in ((LOCAL, RECEIVED, chan.get_latest_ctn(LOCAL)), + (REMOTE, SENT, chan.get_oldest_unrevoked_ctn(LOCAL)), + (REMOTE, SENT, chan.get_latest_ctn(LOCAL)),): + for htlc_id, htlc in chan.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items(): + if not chan.hm.was_htlc_preimage_released(htlc_id=htlc_id, htlc_sender=REMOTE): + continue + if htlc.cltv_expiry - recv_htlc_deadline > local_height: + continue + htlcs_we_could_reclaim[(RECEIVED, htlc_id)] = htlc + # If there is an offered HTLC which has already expired (+ some grace period after), we + # will unilaterally close the channel and time out the HTLC + offered_htlc_deadline = lnutil.NBLOCK_DEADLINE_AFTER_EXPIRY_FOR_OFFERED_HTLCS + for sub, dir, ctn in ((LOCAL, SENT, chan.get_latest_ctn(LOCAL)), + (REMOTE, RECEIVED, chan.get_oldest_unrevoked_ctn(LOCAL)), + (REMOTE, RECEIVED, chan.get_latest_ctn(LOCAL)),): + for htlc_id, htlc in chan.hm.htlcs_by_direction(subject=sub, direction=dir, ctn=ctn).items(): + if htlc.cltv_expiry + offered_htlc_deadline > local_height: + continue + htlcs_we_could_reclaim[(SENT, htlc_id)] = htlc + + total_value_sat = sum([htlc.amount_msat // 1000 for htlc in htlcs_we_could_reclaim.values()]) + num_htlcs = len(htlcs_we_could_reclaim) + min_value_worth_closing_channel_over_sat = max(num_htlcs * 10 * chan.config[REMOTE].dust_limit_sat, + 500_000) + return total_value_sat > min_value_worth_closing_channel_over_sat @log_exceptions async def on_network_update(self, event, *args): t@@ -652,7 +675,7 @@ class LNWallet(LNWorker): for chan in channels: if chan.is_closed(): continue - if chan.get_state() in ["OPEN", "DISCONNECTED"] and self.is_dangerous(chan): + if chan.get_state() != 'CLOSED' and self.should_channel_be_closed_due_to_expiring_htlcs(chan): await self.force_close_channel(chan.channel_id) continue if chan.short_channel_id is None: