What changed, and why it matters
This commit cleans up leftover data after a failed Lightning submarine swap in the Electrum wallet. Previously, failed swaps could leave stale payment information, routing-failure records, and internal index entries behind. The patch removes those leftovers so they cannot accidentally be reused or misinterpreted later. It is a defensive hardening change rather than a fix for a known active attack.
Treat as a routine hardening patch. Users running submarine-swap functionality should update to a version containing this commit. No immediate incident response is indicated by the commit itself, but operators should monitor for any prior anomalous swap behavior.
Security signals we found
Removal of persisted payment/routing state for failed swaps
Deletion of internal index entries that could reference stale swap objects
Defensive cleanup of hold-invoice callbacks and prepay hashes
Commit message frames change as cleanup, not as a security bug fix
Evidence from the diff
The change modifies LNWallet.delete_payment_bundle() and SwapManager.fail_swap(). When a submarine swap fails, the code now: (1) deletes persisted payment info for the swap’s payment_hash instead of saving an OnionRoutingFailure, (2) clears the invoices cache, (3) removes the swap from _swaps, _swaps_by_funding_outpoint, _swaps_by_lockup_address, and (4) if a prepay_hash exists and was not paid, deletes its payment info and the payment bundle. The prior behavior saved a synthetic OnionRoutingFailure and left multiple internal dicts populated.
Changed components
electrum/lnworker.pyelectrum/submarine_swaps.pyInspect captured patch +19 / −6
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index d5a8fc0..a2b8feb 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -2318,6 +2318,13 @@ class LNWallet(LNWorker):
return key_list
return []
+ def delete_payment_bundle(self, payment_hash: bytes) -> None:
+ payment_key = self._get_payment_key(payment_hash)
+ for key_list in self.payment_bundles:
+ if payment_key in key_list:
+ self.payment_bundles.remove(key_list)
+ return
+
def save_preimage(self, payment_hash: bytes, preimage: bytes, *, write_to_disk: bool = True):
if sha256(preimage) != payment_hash:
raise Exception("tried to save incorrect preimage for payment_hash")
diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
index 5ba8e04..e5acbdd 100644
--- a/electrum/submarine_swaps.py
+++ b/electrum/submarine_swaps.py
@@ -42,7 +42,7 @@ from .json_db import StoredObject, stored_in
from . import constants
from .address_synchronizer import TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE
from .fee_policy import FeePolicy
-from .invoices import Invoice
+from .invoices import Invoice, PR_PAID
from .lnonion import OnionRoutingFailure, OnionFailureCode
from .lnsweep import SweepInfo
@@ -370,16 +370,22 @@ class SwapManager(Logger):
self.logger.info(f'failing swap {swap.payment_hash.hex()}: {reason}')
if not swap.is_reverse and swap.payment_hash in self.lnworker.hold_invoice_callbacks:
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
- e = OnionRoutingFailure(code=OnionFailureCode.INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS, data=b'')
- self.lnworker.save_forwarding_failure(payment_key.hex(), failure_message=e)
+ # Peer.maybe_fulfill_htlc will fail incoming htlcs if there is no payment info
+ self.lnworker.delete_payment_info(swap.payment_hash.hex())
+ self.lnworker.clear_invoices_cache()
self.lnwatcher.remove_callback(swap.lockup_address)
if not swap.is_funded():
with self.swaps_lock:
if self._swaps.pop(swap.payment_hash.hex(), None) is None:
self.logger.debug(f"swap {swap.payment_hash.hex()} has already been deleted.")
- # TODO clean-up other swaps dicts, i.e. undo _add_or_reindex_swap()
+ if swap._funding_prevout is not None:
+ self._swaps_by_funding_outpoint.pop(swap._funding_prevout, None)
+ self._swaps_by_lockup_address.pop(swap.lockup_address, None)
+ if swap.prepay_hash is not None:
+ self._prepayments.pop(swap.prepay_hash, None)
+ if self.lnworker.get_payment_status(swap.prepay_hash) != PR_PAID:
+ self.lnworker.delete_payment_info(swap.prepay_hash.hex())
+ self.lnworker.delete_payment_bundle(swap.payment_hash)
@classmethod
def extract_preimage(cls, swap: SwapData, claim_tx: Transaction) -> Optional[bytes]:
Why this scored 42/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.