From 136978e9d06a266a7a85bb6e1b2641fbe0e5a708 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 8 Sep 2023 14:44:52 +0200 Subject: [PATCH] submarine swaps: fail received HTLCs of normal swap htlcs if the swap is still unfunded and the refund delay has expired. --- electrum/lnworker.py | 5 +++- electrum/submarine_swaps.py | 51 ++++++++++++++++++++++++----------- electrum/tests/test_lnpeer.py | 5 ++-- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/electrum/lnworker.py b/electrum/lnworker.py index b4a1317fe..e67b9725c 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -2070,9 +2070,12 @@ class LNWallet(LNWorker): info = PaymentInfo(payment_hash, lightning_amount_sat * 1000, RECEIVED, PR_UNPAID) self.save_payment_info(info, write_to_disk=False) - def register_callback_for_hold_invoice(self, payment_hash: bytes, cb: Callable[[bytes], Awaitable[None]]): + def register_hold_invoice(self, payment_hash: bytes, cb: Callable[[bytes], Awaitable[None]]): self.hold_invoice_callbacks[payment_hash] = cb + def unregister_hold_invoice(self, payment_hash: bytes): + self.hold_invoice_callbacks.pop(payment_hash) + def save_payment_info(self, info: PaymentInfo, *, write_to_disk: bool = True) -> None: key = info.payment_hash.hex() assert info.status in SAVED_PR_STATUS diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index bc97c9657..a20ad441f 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -199,7 +199,7 @@ class SwapManager(Logger): swap._payment_hash = payment_hash self._add_or_reindex_swap(swap) if not swap.is_reverse and not swap.is_redeemed: - self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) + self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback) self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash for k, swap in self.swaps.items(): @@ -251,6 +251,15 @@ class SwapManager(Logger): continue await self.taskgroup.spawn(self.pay_invoice(key)) + def fail_normal_swap(self, swap): + if swap.payment_hash in self.lnworker.hold_invoice_callbacks: + self.logger.info(f'failing normal swap {swap.payment_hash.hex()}') + self.lnworker.unregister_hold_invoice(swap.payment_hash) + payment_secret = self.lnworker.get_payment_secret(swap.payment_hash) + payment_key = swap.payment_hash + payment_secret + self.lnworker.fail_final_onion_forwarding(payment_key) + self.lnwatcher.remove_callback(swap.lockup_address) + @log_exceptions async def _claim_swap(self, swap: SwapData) -> None: assert self.network @@ -260,10 +269,23 @@ class SwapManager(Logger): current_height = self.network.get_local_height() delta = current_height - swap.locktime txos = self.lnwatcher.adb.get_addr_outputs(swap.lockup_address) + for txin in txos.values(): if swap.is_reverse and txin.value_sats() < swap.onchain_amount: - self.logger.info('amount too low, we should not reveal the preimage') + # amount too low, we must not reveal the preimage continue + break + else: + # swap not funded. + if delta >= 0: + if not swap.is_reverse: + # we might have received HTLCs and double spent the funding tx + # in that case we need to fail the HTLCs + self.fail_normal_swap(swap) + txin = None + + if txin: + # the swap is funded swap.funding_txid = txin.prevout.txid.hex() swap._funding_prevout = txin.prevout self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint @@ -298,42 +320,39 @@ class SwapManager(Logger): else: # refund tx if spent_height > 0: - self.logger.info(f'found confirmed refund') - payment_secret = self.lnworker.get_payment_secret(swap.payment_hash) - payment_key = swap.payment_hash + payment_secret - self.lnworker.fail_final_onion_forwarding(payment_key) - + self.fail_normal_swap(swap) + return if delta < 0: # too early for refund - continue + return else: if swap.preimage is None: swap.preimage = self.lnworker.get_preimage(swap.payment_hash) if swap.preimage is None: if funding_conf <= 0: - continue + return key = swap.payment_hash.hex() if -delta <= MIN_LOCKTIME_DELTA: if key in self.invoices_to_pay: # fixme: should consider cltv of ln payment self.logger.info(f'locktime too close {key} {delta}') self.invoices_to_pay.pop(key, None) - continue + return if key not in self.invoices_to_pay: self.invoices_to_pay[key] = 0 - continue + return if self.network.config.TEST_SWAPSERVER_REFUND: # for testing: do not create claim tx - continue + return if spent_height is not None: - continue + return try: tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) except BelowDustLimit: self.logger.info('utxo value below dust threshold') - continue + return self.logger.info(f'adding claim tx {tx.txid()}') self.wallet.adb.add_transaction(tx) swap.spending_txid = tx.txid() @@ -391,7 +410,7 @@ class SwapManager(Logger): invoice=None, prepay=True, ) - self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback) + self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback) return swap, invoice, prepay_invoice def add_normal_swap( @@ -652,7 +671,7 @@ class SwapManager(Logger): data = json.loads(response) async def callback(payment_hash): await self.broadcast_funding_tx(swap, tx) - self.lnworker.register_callback_for_hold_invoice(payment_hash, callback) + self.lnworker.register_hold_invoice(payment_hash, callback) # wait for funding tx lnaddr = lndecode(invoice) while swap.funding_txid is None and not lnaddr.is_expired(): diff --git a/electrum/tests/test_lnpeer.py b/electrum/tests/test_lnpeer.py index 37f5ccceb..75f5c5394 100644 --- a/electrum/tests/test_lnpeer.py +++ b/electrum/tests/test_lnpeer.py @@ -288,7 +288,8 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): _on_maybe_forwarded_htlc_resolved = LNWallet._on_maybe_forwarded_htlc_resolved _force_close_channel = LNWallet._force_close_channel suggest_splits = LNWallet.suggest_splits - register_callback_for_hold_invoice = LNWallet.register_callback_for_hold_invoice + register_hold_invoice = LNWallet.register_hold_invoice + unregister_hold_invoice = LNWallet.unregister_hold_invoice add_payment_info_for_hold_invoice = LNWallet.add_payment_info_for_hold_invoice update_mpp_with_received_htlc = LNWallet.update_mpp_with_received_htlc @@ -784,7 +785,7 @@ class TestPeer(ElectrumTestCase): w2.save_preimage(payment_hash, preimage) else: raise OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'') - w2.register_callback_for_hold_invoice(payment_hash, cb) + w2.register_hold_invoice(payment_hash, cb) if test_bundle: lnaddr2, pay_req2 = self.prepare_invoice(w2)