Skip to content

Somewhat clean up closure pipelines #3881

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

TheBlueMatt
Copy link
Collaborator

Still have a few commits on top here but this ended up getting quite big so I'll do them in a followup. There's a pile of things to DRY things up and make small wins here or there, but the main two commits are:

Drop the (broken) ability to disable broadcast when FC'ing a chan

`ChannelManager` has two public methods to force-close a (single)
channel - `force_close_broadcasting_latest_txn` and
`force_close_without_broadcasting_txn`. The second exists to allow
a user who has restored a stale backup to (sort of) recover their
funds by starting, force-closing channels without broadcasting
their latest state, and only then reconnecting to peers (which
would otherwise cause a panic due to the stale state).

To my knowledge, no one has ever implemented this (fairly
half-assed) recovery flow, and more importantly its rather
substantially broken. `ChannelMonitor` has never tracked any
concept of whether a channel is stale, and if we connect blocks
towards an HTLC time-out it will immediately broadcast the stale
state anyway.

Finally, we really shouldn't encourage users to try to "recover"
from a stale state by running an otherwise-operational node on the
other end. Rather, we should formalize such a recovery flow by
having a `ChainMonitor` variant that takes a list of
`ChannelMonitor`s and claims available funds, dropping the
`ChannelManager` entirely.

While work continues towards that goal, having long-broken
functionality in `ChannelManager` is also not acceptable, so here
we simply remove the "without broadcasting" force-closure version.

Fixes #2875

and

Decide on close-broadcasting commitment txn based on channel state

In a previous commit, we removed the ability for users to pick
whether we will broadcast a commitment transaction on channel
closure. However, that doesn't mean that there is no value in never
broadcasting commitment transactions on channel closure. Rather, we
use it to avoid broadcasting transactions which we know cannot
confirm if the channel's funding transaction was not broadcasted.

Here we make this relationship more formal by splitting the
force-closure handling logic in `Channel` into the existing
`ChannelContext::force_shutdown` as well as a new
`ChannelContext::abandon_unfunded_chan`.
`ChannelContext::force_shutdown` is the only public method, but it
delegates to `abandon_unfunded_chan` based on the channel's state.

This has the nice side effect of avoiding commitment transaction
broadcasting when a batch open fails to get past the funding stage.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 23, 2025

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

@ldk-reviews-bot ldk-reviews-bot requested a review from jkczyz June 23, 2025 01:58
@tnull tnull requested review from tnull and removed request for jkczyz June 23, 2025 06:12
Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Did a first pass.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be good to document all these subtle API changes in a pending_changelog, as users might be leaning on a certain error variant.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems unaddressed so far?

@@ -4350,85 +4360,66 @@ where
/// `peer_msg` should be set when we receive a message from a peer, but not set when the
/// user closes, which will be re-exposed as the `ChannelClosed` reason.
#[rustfmt::skip]
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, etc.

@tnull
Copy link
Contributor

tnull commented Jun 26, 2025

Still have a few commits on top here but this ended up getting quite big so I'll do them in a followup.

Mind putting that PR up as draft already? Would help me gauge whether I should hold off on simple close related refactors until these two PRs are in.

`ChannelManager::force_close_channel_with_peer` takes the peer's
node_id as a parameter and then returns it, presumably leftover
from before we stored channels indexed by peers.

Here we simply drop the return value.
When a channel is "coop" closed prior to it being funded, it is
closed immediately. Instead of reporting
`ClosureReason::*InitiatedCooperativeClosure` (which would be
somewhat misleading), we report
`ClosureReason::CounterpartyCoopClosedUnfundedCHannel` when our
counterparty does it. However, when we do it, we report
`ClosureReason::HolderForceClosed`, which is highly confusing given
the user did *not* call a force-closure method.

Instead, here, we add a
`ClosureReason::LocallyCoopClosedUnfundedChannel` to match the
`CounterpartyCoopClosedUnfundedCHannel` variant and use it.
`ClosureReason::HolderForceClosed` says explicitly in its docs
"Closure generated from `ChannelManager...`, called by the user."
However, we were using it extensively when we fail a channel due to
errors handling messages or generating our own responses.

Here, we convert these cases to the catch-all
`ClosureReason::ProcessingError` which exists to communicate errors
including a string that describes what happened.

This should substantially improve debug-ability in closure cases
by avoiding having to pull up logs.
Here we fix a few cases of swallowing appropriate `ClosureReason`s
and expand `ClosureReason::FundingTimedOut` to include when we
didn't get a channel funded in time, rather than using
`HolderForceClosed`.
In d17e759 we added the ability
for users to decide which message to send to peers when we
force-close a channel. Here we pipe that message back out through
the channel closure event via a new `message` field in
`ClosureReason::HolderForceClosed`.

This is nice to have but more importantly will be used in a coming
commit to simplify the internal interface to the force-closure
logic.
Removes totally unnecessary `#[inline]`s  on `MsgHandleErrInternal`
methods (LLVM will decide for us if it makes sense to inline
something, explicit `#[inline(always)]` belongs only on incredibly
performance-sensitive code and `#[inline]` belongs only on short
public methods that should be inlined into downstream crates) and
`rustfmt` `MsgHandleErrInternal::from_chan_no_close`.
`ChannelManager` has two public methods to force-close a (single)
channel - `force_close_broadcasting_latest_txn` and
`force_close_without_broadcasting_txn`. The second exists to allow
a user who has restored a stale backup to (sort of) recover their
funds by starting, force-closing channels without broadcasting
their latest state, and only then reconnecting to peers (which
would otherwise cause a panic due to the stale state).

To my knowledge, no one has ever implemented this (fairly
half-assed) recovery flow, and more importantly its rather
substantially broken. `ChannelMonitor` has never tracked any
concept of whether a channel is stale, and if we connect blocks
towards an HTLC time-out it will immediately broadcast the stale
state anyway.

Finally, we really shouldn't encourage users to try to "recover"
from a stale state by running an otherwise-operational node on the
other end. Rather, we should formalize such a recovery flow by
having a `ChainMonitor` variant that takes a list of
`ChannelMonitor`s and claims available funds, dropping the
`ChannelManager` entirely.

While work continues towards that goal, having long-broken
functionality in `ChannelManager` is also not acceptable, so here
we simply remove the "without broadcasting" force-closure version.

Fixes lightningdevkit#2875
While most of our closure logic mostly lives in
`locked_close_chan`, some important steps happen in
`convert_channel_err` and `handle_error` (mostly broadcasting a
channel closure `ChannelUpdate` and sending a relevant error
message to our peer).

While its fine to use `locked_close_chan` directly when we
manually write out the relevant extra steps, its nice to avoid it
to DRY up some of our closure logic, which we do here.
`Channel`'s new `FundingScope` exists to store both the channel's
live funding information as well as any in-flight splices. In order
to keep the patches which introduced and began using it simple, it
was exposed outside of `channel.rs` to `channelmanager.rs`,
breaking the `Channel` abstraction somewhat.

Here we remove one case of `FundingScope` being passed around
`channelmanager.rs` (in the hopes of eventually keeping it entirely
contained within `channel.rs`). Specifically, we remove the
`FundingScope` parameter from `locked_close_channel`.
In a previous commit, we removed the ability for users to pick
whether we will broadcast a commitment transaction on channel
closure. However, that doesn't mean that there is no value in never
broadcasting commitment transactions on channel closure. Rather, we
use it to avoid broadcasting transactions which we know cannot
confirm if the channel's funding transaction was not broadcasted.

Here we make this relationship more formal by splitting the
force-closure handling logic in `Channel` into the existing
`ChannelContext::force_shutdown` as well as a new
`ChannelContext::abandon_unfunded_chan`.
`ChannelContext::force_shutdown` is the only public method, but it
delegates to `abandon_unfunded_chan` based on the channel's state.

This has the nice side effect of avoiding commitment transaction
broadcasting when a batch open fails to get past the funding stage.
@TheBlueMatt TheBlueMatt force-pushed the 2025-06-fc-always-broadcast branch from e73434f to 544175b Compare June 28, 2025 21:06
Copy link
Collaborator Author

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

Mind putting that PR up as draft already? Would help me gauge whether I should hold off on simple close related refactors until these two PRs are in.

Yea, I wanted to do a few further things, but I just don't think I have time. I put up what I have at #3899.

Copy link

codecov bot commented Jun 28, 2025

Codecov Report

Attention: Patch coverage is 85.26703% with 80 lines in your changes missing coverage. Please review.

Project coverage is 88.89%. Comparing base (61a37b1) to head (544175b).

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 60.90% 41 Missing and 2 partials ⚠️
lightning/src/ln/channelmanager.rs 82.67% 25 Missing and 10 partials ⚠️
lightning/src/events/mod.rs 60.00% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3881      +/-   ##
==========================================
+ Coverage   88.86%   88.89%   +0.03%     
==========================================
  Files         165      165              
  Lines      118886   118921      +35     
  Branches   118886   118921      +35     
==========================================
+ Hits       105650   105718      +68     
+ Misses      10911    10878      -33     
  Partials     2325     2325              

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

Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

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

Fixups look good, will ping @wpaulino as a secondary reviewer here, especially given the size of the patchset in Channel.

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems unaddressed so far?

@@ -3160,6 +3162,11 @@ macro_rules! handle_error {
///
/// Note that this step can be skipped if the channel was never opened (through the creation of a
/// [`ChannelMonitor`]/channel funding transaction) to begin with.
///
/// For non-coop-close cases, you should generally prefer to call `convert_channel_err` and
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we maybe also update the docs on finish_close_channel to mention the convert alternative?

}
}

/// Shuts down this channel (no more calls into this Channel may be made afterwards except
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Channel

}

/// Shuts down this channel (no more calls into this Channel may be made afterwards except
/// those explicitly stated to be alowed after shutdown, eg some simple getters).
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Decide on 'e.g.'/'i.e.' vs. 'eg.'/'ie.'.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @wpaulino! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

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.

3 participants