diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index bd94eedfa..3f8c35242 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -2119,6 +2119,7 @@ class Peer(Logger): else: raise Exception(f"unexpected {mpp_resolution=}") + # TODO check against actual min_final_cltv_expiry_delta from invoice (and give 2-3 blocks of leeway?) if local_height + MIN_FINAL_CLTV_DELTA_ACCEPTED > htlc.cltv_abs: if not already_forwarded: log_fail_reason(f"htlc.cltv_abs is unreasonably close") diff --git a/electrum/lnworker.py b/electrum/lnworker.py index f79560043..1d799de38 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2146,9 +2146,10 @@ class LNWallet(LNWorker): payment_hash: bytes, amount_msat: Optional[int], message: str, - expiry: int, + expiry: int, # expiration of invoice (in seconds, relative) fallback_address: Optional[str], channels: Optional[Sequence[Channel]] = None, + min_final_cltv_expiry_delta: Optional[int] = None, ) -> Tuple[LnAddr, str]: assert isinstance(payment_hash, bytes), f"expected bytes, but got {type(payment_hash)}" @@ -2169,12 +2170,14 @@ class LNWallet(LNWorker): amount_btc = amount_msat/Decimal(COIN*1000) if amount_msat else None if expiry == 0: expiry = LN_EXPIRY_NEVER + if min_final_cltv_expiry_delta is None: + min_final_cltv_expiry_delta = MIN_FINAL_CLTV_DELTA_FOR_INVOICE lnaddr = LnAddr( paymenthash=payment_hash, amount=amount_btc, tags=[ ('d', message), - ('c', MIN_FINAL_CLTV_DELTA_FOR_INVOICE), + ('c', min_final_cltv_expiry_delta), ('x', expiry), ('9', invoice_features), ('f', fallback_address), diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 84ecd7cf8..6b5fb5480 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -50,9 +50,11 @@ LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs MIN_LOCKTIME_DELTA = 60 LOCKTIME_DELTA_REFUND = 70 MAX_LOCKTIME_DELTA = 100 +MIN_FINAL_CLTV_DELTA_FOR_CLIENT = 3 * 144 # note: put in invoice, but is not enforced by receiver in lnpeer.py assert MIN_LOCKTIME_DELTA <= LOCKTIME_DELTA_REFUND <= MAX_LOCKTIME_DELTA assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_ACCEPTED assert MAX_LOCKTIME_DELTA < lnutil.MIN_FINAL_CLTV_DELTA_FOR_INVOICE +assert MAX_LOCKTIME_DELTA < MIN_FINAL_CLTV_DELTA_FOR_CLIENT # The script of the reverse swaps has one extra check in it to verify @@ -443,6 +445,7 @@ class SwapManager(Logger): our_privkey: bytes, prepay: bool, channels: Optional[Sequence['Channel']] = None, + min_final_cltv_expiry_delta: Optional[int] = None, ) -> Tuple[SwapData, str, str]: """creates a hold invoice""" if prepay: @@ -458,6 +461,7 @@ class SwapManager(Logger): expiry=300, fallback_address=None, channels=channels, + min_final_cltv_expiry_delta=min_final_cltv_expiry_delta, ) # add payment info to lnworker self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) @@ -471,6 +475,7 @@ class SwapManager(Logger): expiry=300, fallback_address=None, channels=channels, + min_final_cltv_expiry_delta=min_final_cltv_expiry_delta, ) self.lnworker.bundle_payments([payment_hash, prepay_hash]) self.prepayments[prepay_hash] = payment_hash @@ -666,6 +671,13 @@ class SwapManager(Logger): our_privkey=refund_privkey, prepay=False, channels=channels, + # When the client is doing a normal swap, we create a ln-invoice with larger than usual final_cltv_delta. + # If the user goes offline after broadcasting the funding tx (but before it is mined and + # the server claims it), they need to come back online before the held ln-htlc expires (see #8940). + # If the held ln-htlc expires, and the funding tx got confirmed, the server will have claimed the onchain + # funds, and the ln-htlc will be timed out onchain (and channel force-closed). i.e. the user loses the swap + # amount. Increasing the final_cltv_delta the user puts in the invoice extends this critical window. + min_final_cltv_expiry_delta=MIN_FINAL_CLTV_DELTA_FOR_CLIENT, ) return swap, invoice