lnworker/lnrouter: update liquidity hints in htlc callbacks
What changed, and why it matters
This commit fixes a bug in Electrum's Lightning payment routing. Previously, when a multi-part payment succeeded, only the first successful payment chunk properly updated the wallet's internal channel-liquidity bookkeeping. Later chunks were never cleaned up, so the wallet kept treating those routes as if they still had money in flight. Over time this could make the wallet wrongly avoid or penalize channels that had actually worked fine, degrading payment reliability. The fix moves the cleanup into the proper success/failure callbacks so every chunk is accounted for.
Apply the patch. Users relying on Lightning multi-part payments should upgrade to avoid degraded routing decisions. No immediate remote exploit is evident, but payment reliability and fee costs may be affected until patched.
Security signals we found
Resource/accounting leak in routing state
Incorrect penalty of successful Lightning routes
Potential denial-of-service against local payment reliability
Logic bug in multi-part payment cleanup
Evidence from the diff
The patch moves liquidity-hint updates from _process_htlc_log() into the htlc_fulfilled and htlc_failed callbacks. _process_htlc_log() raised PaymentSuccess() on the first fulfilled HTLC, so only that HTLC’s route had its inflight counter decremented. Subsequent fulfilled HTLCs leaked their inflight counts in LiquidityHintMgr, causing successful routes to be incorrectly penalized. The change also resets the forward/backward inflight counters in reset_liquidity_hints().
Changed components
electrum/lnrouter.pyelectrum/lnworker.pyLiquidityHintMgrLNWallet._process_htlc_log()LNWallet.htlc_fulfilled()LNWallet.htlc_failed()LNWallet._handle_sending_htlc_success()LNWallet._handle_sending_htlc_failed()Inspect captured patch +8 / −12
diff --git a/electrum/lnrouter.py b/electrum/lnrouter.py
index 790303e..1645065 100644
--- a/electrum/lnrouter.py
+++ b/electrum/lnrouter.py
@@ -371,6 +371,8 @@ class LiquidityHintMgr:
def reset_liquidity_hints(self):
for k, v in self._liquidity_hints.items():
v.hint_timestamp = 0
+ v._inflight_htlcs_forward = 0
+ v._inflight_htlcs_backward = 0
def __repr__(self):
string = "liquidity hints:\n"
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index db5d29f..4dc67ae 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -2093,14 +2093,6 @@ class LNWallet(Logger):
or OnionRoutingFailure (if forwarding trampoline).
"""
if htlc_log.success:
- if self.network.path_finder:
- # TODO: report every route to liquidity hints for mpp
- # in the case of success, we report channels of the
- # route as being able to send the same amount in the future,
- # as we assume to not know the capacity
- self.network.path_finder.update_liquidity_hints(htlc_log.route, htlc_log.amount_msat)
- # remove inflight htlcs from liquidity hints
- self.network.path_finder.update_inflight_htlcs(htlc_log.route, add_htlcs=False)
raise PaymentSuccess()
# htlc failed
# if we get a tmp channel failure, it might work to split the amount and try more routes
@@ -2168,7 +2160,7 @@ class LNWallet(Logger):
self.logger.info(f'adding active forwarding {fw_payment_key}')
self.active_forwardings[fw_payment_key].append(htlc_key)
if self.network.path_finder:
- # add inflight htlcs to liquidity hints
+ # add inflight htlcs to liquidity hints; removed again in htlc_fulfilled/htlc_failed
self.network.path_finder.update_inflight_htlcs(shi.route, add_htlcs=True)
util.trigger_callback('htlc_added', chan, htlc, SENT)
@@ -2183,9 +2175,6 @@ class LNWallet(Logger):
assert self.channel_db # cannot be in trampoline mode
assert self.network.path_finder
- # remove inflight htlcs from liquidity hints
- self.network.path_finder.update_inflight_htlcs(route, add_htlcs=False)
-
code, data = failure_msg.code, failure_msg.data
# TODO can we use lnmsg.OnionWireSerializer here?
# TODO update onion_wire.csv
@@ -3168,6 +3157,9 @@ class LNWallet(Logger):
shi = self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id))
if shi and htlc_id in chan.onion_keys:
chan.pop_onion_key(htlc_id)
+ if self.network.path_finder:
+ self.network.path_finder.update_liquidity_hints(shi.route, shi.amount_receiver_msat)
+ self.network.path_finder.update_inflight_htlcs(shi.route, add_htlcs=False)
payment_key = payment_hash + shi.payment_secret_orig
paysession = self._paysessions[payment_key]
q = paysession.sent_htlcs_q
@@ -3214,6 +3206,8 @@ class LNWallet(Logger):
shi = self.sent_htlcs_info.get((payment_hash, chan.short_channel_id, htlc_id))
if shi and htlc_id in chan.onion_keys:
onion_key = chan.pop_onion_key(htlc_id)
+ if self.network.path_finder:
+ self.network.path_finder.update_inflight_htlcs(shi.route, add_htlcs=False)
payment_okey = payment_hash + shi.payment_secret_orig
paysession = self._paysessions[payment_okey]
q = paysession.sent_htlcs_q
Why this scored 34/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.