Skip to content

Check in-flight monitor updates in force_shutdown for LocalAnnounced HTLCs#4434

Open
joostjager wants to merge 1 commit intolightningdevkit:mainfrom
joostjager:fix-force-close-in-progress-monitor-drops-htlc
Open

Check in-flight monitor updates in force_shutdown for LocalAnnounced HTLCs#4434
joostjager wants to merge 1 commit intolightningdevkit:mainfrom
joostjager:fix-force-close-in-progress-monitor-drops-htlc

Conversation

@joostjager
Copy link
Contributor

Closes #4431

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Feb 23, 2026

👋 Thanks for assigning @TheBlueMatt 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.

@joostjager joostjager force-pushed the fix-force-close-in-progress-monitor-drops-htlc branch from 2d36c03 to cce8ccb Compare February 23, 2026 10:08
@codecov
Copy link

codecov bot commented Feb 23, 2026

Codecov Report

❌ Patch coverage is 93.54839% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.97%. Comparing base (ec03159) to head (da66b8e).

Files with missing lines Patch % Lines
lightning/src/chain/channelmonitor.rs 83.33% 0 Missing and 1 partial ⚠️
lightning/src/ln/channel.rs 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4434      +/-   ##
==========================================
+ Coverage   85.93%   85.97%   +0.03%     
==========================================
  Files         159      159              
  Lines      104693   104712      +19     
  Branches   104693   104712      +19     
==========================================
+ Hits        89972    90022      +50     
+ Misses      12213    12187      -26     
+ Partials     2508     2503       -5     
Flag Coverage Δ
tests 85.97% <93.54%> (+0.03%) ⬆️

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.

@joostjager joostjager marked this pull request as ready for review February 23, 2026 13:49
@joostjager joostjager requested a review from wpaulino February 23, 2026 17:34
chan.force_shutdown(reason)
let in_flight = in_flight_monitor_updates
.get(&chan_id)
.map(|(_, updates)| updates.iter().collect::<Vec<_>>())
Copy link
Contributor

@wpaulino wpaulino Feb 23, 2026

Choose a reason for hiding this comment

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

Do we really need this allocation? Can we just pass the iterator to force_shutdown instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good one, fixed

@ldk-reviews-bot
Copy link

👋 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.

@wpaulino wpaulino requested a review from TheBlueMatt February 23, 2026 20:03
@joostjager joostjager force-pushed the fix-force-close-in-progress-monitor-drops-htlc branch from cce8ccb to ad3036e Compare February 23, 2026 21:11
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Hmmmmmmmmm, its really not quite so simple. If a monitor update is InProgress we generally expect the in-memory ChannelMonitor to have actually seen the new state update (though we expect that on restart there's a good chance it won't have that information and it'll be replayed). That means the monitor is theoretically "in charge" of resolving the HTLCs in question. While I think this patch is correct, I'm a bit hesitant to end up with two things in charge of resolving a specific HTLC. ISTM it also wouldn't be so complicated to immediately mark HTLCs as failed in the monitor upon receiving updates that try to add new HTLCs after a channel has been closed.

@joostjager
Copy link
Contributor Author

ISTM it also wouldn't be so complicated to immediately mark HTLCs as failed in the monitor upon receiving updates that try to add new HTLCs after a channel has been closed.

Isn't the problem with this that the monitor can't see whether the commitment was already sent to the peer or blocked by async persistence?

@TheBlueMatt
Copy link
Collaborator

Right, the monitor has to assume it may have been sent to the peer. That's fine, though, it should be able to fail the HTLC(s) once the commitment transaction is locked.

@joostjager
Copy link
Contributor Author

Hmm, it seems that the original issue isn't an issue then. I confirmed that mining the force close commitment tx indeed generates the payment failed event. Or is there still something missing?

@TheBlueMatt
Copy link
Collaborator

In the general case (ChannelMonitor in-memory in the same machine that runs ChannelManager) I believe there is not a runtime issue, no. I was thinking you'd identified an issue in the crash case, though (where we crash without persisting the monitor update and on restart lose it) but actually I'm not sure this is any different from existing payments logic - if you start a payment and nothing persists on restart its expected to be gone and downstream code has to handle that.

@joostjager
Copy link
Contributor Author

The one that I identified wasn't a crash case. An assertion in the fuzzer identified that a payment was lost, but with current understanding I think it's just that the fuzzer didn't push the force-close forward enough (mine). Will close it for now and can look again if the payment remains lost even with confirmation of the commit tx. Unit test did confirm that it should just work.

@joostjager
Copy link
Contributor Author

Reopening as this fix is now required for #4351.

@joostjager joostjager reopened this Mar 2, 2026
@joostjager
Copy link
Contributor Author

Hmmmmmmmmm, its really not quite so simple. If a monitor update is InProgress we generally expect the in-memory ChannelMonitor to have actually seen the new state update (though we expect that on restart there's a good chance it won't have that information and it'll be replayed). That means the monitor is theoretically "in charge" of resolving the HTLCs in question. While I think this patch is correct, I'm a bit hesitant to end up with two things in charge of resolving a specific HTLC. ISTM it also wouldn't be so complicated to immediately mark HTLCs as failed in the monitor upon receiving updates that try to add new HTLCs after a channel has been closed.

Now that we've decided that for deferred writes we no longer want to expect the in-memory ChannelMonitor to have seen the changes, I think the above doesn't apply anymore, and we might have to accept that two things are in charge for resolving a specific HTLC.

if let OutboundHTLCState::LocalAnnounced(_) = htlc.state {
for update in self.blocked_monitor_updates.iter() {
for update in update.update.updates.iter() {
let blocked_iter = self.blocked_monitor_updates.iter().map(|u| &u.update);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do wonder if it is also possible for outbound htlcs to have already progressed to the Committed or later states? In that case, we might also want to catch that here.

@joostjager joostjager requested a review from TheBlueMatt March 2, 2026 08:13
@joostjager joostjager force-pushed the fix-force-close-in-progress-monitor-drops-htlc branch 2 times, most recently from 6421a28 to 8dac2fa Compare March 2, 2026 08:20
events.pop().unwrap(),
Event::ChannelClosed { reason: ClosureReason::CommitmentTxConfirmed, .. }
));
expect_payment_failed_conditions_event(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This now no longer needs to wait for the force close to confirm.

…HTLCs

When building dropped_outbound_htlcs during force_shutdown,
LocalAnnounced HTLCs were only checked against blocked_monitor_updates.
This relied on the implicit contract that in-progress monitor updates
have always been applied to the in-memory ChannelMonitor already. If
InProgress is returned without the monitor knowing about the update yet,
the HTLC is silently dropped with no PaymentPathFailed or PaymentFailed
event generated.

Pass the in-flight monitor updates for the channel into force_shutdown
so both blocked and in-flight updates are checked. This ensures
LocalAnnounced HTLCs whose monitor updates have not yet been applied
are properly included in dropped_outbound_htlcs and failed back to
the sender.

Closes lightningdevkit#4431

AI tools were used in preparing this commit.
@joostjager joostjager force-pushed the fix-force-close-in-progress-monitor-drops-htlc branch from 8dac2fa to da66b8e Compare March 2, 2026 08:23
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Now that we've decided that for deferred writes we no longer want to expect the in-memory ChannelMonitor to have seen the changes, I think the above doesn't apply anymore, and we might have to accept that two things are in charge for resolving a specific HTLC.

I don't think so. Instead, what we'll need to do is detect a new local counterparty state ChannelMontiorUpdate after the channel has close and track it to fail the HTLCs from it once the commitment transaction has 6 confs.

@TheBlueMatt
Copy link
Collaborator

Oh actually no I don't think we need to wait for 6 confs since we never sent the message, ie we can fail it immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Payment silently stuck when channel force-closed during in-progress monitor update

4 participants