') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Fix race condition causing async payment failure by valentinewallace · Pull Request #4106 · lightningdevkit/rust-lightning · GitHub
Skip to content

Fix race condition causing async payment failure - #4106

Merged
joostjager merged 2 commits into
lightningdevkit:mainfrom
valentinewallace:2025-08-async-sender-fix-race
Sep 25, 2025
Merged

Fix race condition causing async payment failure#4106
joostjager merged 2 commits into
lightningdevkit:mainfrom
valentinewallace:2025-08-async-sender-fix-race

Conversation

@valentinewallace

Copy link
Copy Markdown
Contributor

As the LSP of an async sender, when we receive an update_add with the hold_htlc flag set, after its onion is decoded we transition the pending HTLC to the ChannelManager::pending_intercepted_htlcs. However, if we receive the release_held_htlc message from the receiver before we've had a chance to make this transition, we'll fail to release the HTLC and it will sit in the pending intercepts map until it is failed backwards.

To fix this race condition, if we receive release_held_htlc from the recipient we'll not only check the pending_intercepted_htlcs map for the presence of this HTLC but also check the map where we keep HTLCs prior to their onions being decoded.

@ldk-reviews-bot

ldk-reviews-bot commented Sep 22, 2025

Copy link
Copy Markdown

👋 Thanks for assigning @joostjager as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

Comment threadlightning/src/ln/channelmanager.rs Outdated
@codecov

codecovBot commented Sep 22, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.85714% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.53%. Comparing base (e82ef2c) to head (ade1f34).
⚠️ Report is 92 commits behind head on main.

Files with missing linesPatch %Lines
lightning/src/ln/channelmanager.rs89.23%7 Missing ⚠️
lightning/src/ln/async_payments_tests.rs97.56%1 Missing ⚠️
Additional details and impacted files
@@ Coverage Diff @@## main #4106 +/- ##
=========================================
Coverage 88.53% 88.53% =========================================
Files 175 179 +4 Lines 132702 134407 +1705 Branches 132702 134407 +1705 =========================================
+ Hits 117484 118994 +1510 - Misses 12618 12658 +40 - Partials 2600 2755 +155 
FlagCoverage Δ
fuzzing21.78% <26.76%> (?)
tests88.37% <92.85%> (-0.16%)⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@valentinewallace
valentinewallaceforce-pushed the 2025-08-async-sender-fix-race branch from 1be75bd to 5ed1b9fCompareSeptember 22, 2025 20:03
@valentinewallacevalentinewallace added the weekly goal Someone wants to land this this week label Sep 22, 2025
@TheBlueMattTheBlueMatt added this to the 0.2 milestone Sep 22, 2025
Comment threadlightning/src/blinded_path/message.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

👋 The first review has been submitted!

Do you think this PR is ready for a second reviewer? If so, click here to assign a second reviewer.

joostjager
joostjager previously approved these changes Sep 24, 2025
Comment threadlightning/src/ln/channelmanager.rs
joostjager
joostjager previously approved these changes Sep 24, 2025
Comment threadlightning/src/ln/async_payments_tests.rs Outdated
lock_in_htlc_for_static_invoice(&static_invoice_om, peer_id, sender, sender_lsp);

// The LSP has not transitioned the HTLC to the intercepts map internally because
// process_pending_htlc_forwards has not been called.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Manual message passing is really an advantage here.

Comment threadlightning/src/blinded_path/message.rs Outdated
@ldk-reviews-bot

Copy link
Copy Markdown

✅ Added second reviewer: @jkczyz

@joostjager

Copy link
Copy Markdown
Contributor

This PR also unlocks more e2e testing in ldk-node.

@valentinewallace

Copy link
Copy Markdown
ContributorAuthor

@jkczyz understandable if it doesn't make sense for you to review here, feel free to reroll the bot's assignment

@TheBlueMatt
TheBlueMatt requested review from TheBlueMatt and removed request for jkczyzSeptember 24, 2025 19:50
Comment threadlightning/src/ln/channelmanager.rs Outdated
macro_rules! handle_monitor_update_completion {
($self: ident, $peer_state_lock: expr, $peer_state: expr, $per_peer_state_lock: expr, $chan: expr) => { {
let channel_id = $chan.context.channel_id();
let short_channel_id = $chan.funding.get_short_channel_id().unwrap_or($chan.context().outbound_scid_alias());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just change decode_update_add_htlcs to use the outbound alias? That won't change and also it looks like the SCID itself is basically unused, so there's no reason to use the real one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh fuck this is a mess wrt splicing. Please definitely do this #4121

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be addressed, though we don't move to (PublicKey, ChannelId) here

Since we have added/are adding splicing support, the scid of a channel is
liable to change post-splice.
Some maps in the ChannelManager are keyed by the scid of a channel, which is an
issue now -- if we forward an HTLC from a channel and then splice that
channel before the HTLC is resolved, we'll end up with an HTLC source with an
scid that doesn't correspond to any open channel. This may result in loss of
the HTLC resolution.
The outbound scid alias of a channel is stable even post-splice, so for the
short term here we switch to using that instead. In the medium term we should
update these maps to use (PublicKey, ChannelId) like everything else.
We don't always use the alias for outbound forwarded HTLCs, since we tend to
use whatever outbound scid is in the onion. That's fine because we properly
handle the case where the outbound channel cannot be found; the main problem is
in inbound HTLCs and forgetting their resolution.
As the LSP of an async sender, when we receive an update_add with the hold_htlc
flag set, after its onion is decoded we transition the pending HTLC to the
ChannelManager::pending_intercepted_htlcs. However, if we receive the
release_held_htlc message from the receiver *before* we've had a chance to make
this transition, we'll fail to release the HTLC and it will sit in the pending
intercepts map until it is failed backwards.
To fix this race condition, if we receive release_held_htlc from the recipient
we'll not only check the pending_intercepted_htlcs map for the presence of this
HTLC but also check the map where we keep HTLCs prior to their onions being
decoded.
let mut decode_update_add_htlcs = None;
if !pending_update_adds.is_empty() {
decode_update_add_htlcs = Some((short_channel_id, pending_update_adds));
decode_update_add_htlcs = Some((outbound_scid_alias, pending_update_adds));

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is the only place we key to-be-decoded update_adds with an scid, which should happen for all inbound HTLCs. So I think the source scid will always be the alias now.

@joostjagerjoostjager left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second commit re-acked. First commit I need some context, which I'll get offline. Will approve optimistically for now, to keep moving.

@joostjager
joostjager merged commit 9514637 into lightningdevkit:mainSep 25, 2025
25 checks passed
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 6, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 6, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 7, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 8, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 8, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 8, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 8, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Signed-off-by: Elias Rohrer <dev@tnull.de>
valentinewallace added a commit to valentinewallace/rust-lightning that referenced this pull request Apr 8, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
tnull added a commit to tnull/rust-lightning that referenced this pull request Apr 9, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
valentinewallace added a commit to valentinewallace/rust-lightning that referenced this pull request Apr 9, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
valentinewallace added a commit to valentinewallace/rust-lightning that referenced this pull request Apr 9, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
valentinewallace added a commit to valentinewallace/rust-lightning that referenced this pull request Apr 9, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
valentinewallace added a commit to valentinewallace/rust-lightning that referenced this pull request Apr 9, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
valentinewallace added a commit to valentinewallace/rust-lightning that referenced this pull request Apr 10, 2026
Handle `ReleaseHeldHtlc` messages that arrive before the sender-side LSP
has even queued the held HTLC for onion decoding. Unlike lightningdevkit#4106, which
covers releases arriving after the HTLC is in `decode_update_add_htlcs`
but before it reaches `pending_intercepted_htlcs`, this preserves
releases that arrive one step earlier and would otherwise be dropped as
HTLC not found.
Co-Authored-By: HAL 9000
Co-Authored-By: Elias Rohrer <dev@tnull.de>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

weekly goalSomeone wants to land this this week

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

@valentinewallace@ldk-reviews-bot@joostjager@TheBlueMatt@elnosh