Skip to content

Clean up and split up is_pre_funded_state #4021

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 5 commits into
base: main
Choose a base branch
from

Conversation

TheBlueMatt
Copy link
Collaborator

This turns out to have been kinda buggy, and impossible to read the code, so clean it up a bunch.

`Channel::is_pre_funded_state` is used to mean several different
things. In this case, its used to skip all the `shutdown` logic as
the funding transaction can't possibly have been broadcasted so
there's really no ned to try to sign a transaction spending it.

Here, we really want to capture any channel in `NegotiatedFunding`
or any V1 channel in `FundingNegotiated` or, finally, any V2
channel in `FundingNegotiated` where we haven't yet sent our
signatures (which is not captured in `is_pre_funded_state`).

Instead of a new helper, we just check the states directly in
`shutdown` handling.
`Channel::is_pre_funded_state` is used to mean several different
things. In this case its used to decide if we should provide a
`ChannelMonitorUpdate` marking a channel as closed when we go to
force-close it.

Here, we want to capture exactly when the original `ChannelMonitor`
is first created, but were doing so indirectly by looking at the
channel's state. Worse, `is_pre_funded_state` got updated to be
false whenever there is an interctive signing session, which isn't
correct for this use - we may have an interactive signing session
but have already persisted the original `ChannelMonitor` when we
received the first `commitment_signed`.

Instead, we just move to examining
`cur_counterparty_commitment_transaction_number` which is
decrementing for the first time at exactly the time we create the
original `ChannelMonitor`, so it provides a much simpler test.

Fixes lightningdevkit#3880
`Channel::is_pre_funded_state` is used to mean several different
things. In the past few commits we stopped using it for a few
conflicting uses, but here we break out the remaining uses and
rename the remnants for clarity.

`is_funding_broadcast` was using `is_pre_funded_state` and was then
later used to decide if the `Channel` could be written to disk
(because it can be resumed on restart), if we should broadcast a
force-close transaction, and when to emit a `ChannelPending` event.

These were also somewhat divergent - we shouldn't generate a
`ChannelReady` event or broadcast a force-closing transaction until
we've actually broadcasted but want to write the `Channel` to disk
once we enter funding signature exchange for dual-funded open.

Thus, the ability to write a `Channel` to disk is provided by a new
`can_resume_on_restart` method. Then, `is_funding_broadcast`  is
updated to only consider funding broadcasted after we provide our
funding signatures (i.e. the funding *could* have been
broadcasted). This is still a bit early to generate a
`ChannelPending` event (as the funding may not actually have been
broadcasted yet), but its better than it was.

Finally, the remaining `is_pre_funded_state` is renamed
`can_resume_on_reconnect`, which has slightly different semantics
than on-restart channels in batch opens.
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Aug 18, 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.

Copy link

codecov bot commented Aug 18, 2025

Codecov Report

❌ Patch coverage is 90.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.77%. Comparing base (e1a31e1) to head (9e89ac9).
⚠️ Report is 32 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 90.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4021      +/-   ##
==========================================
- Coverage   88.84%   88.77%   -0.07%     
==========================================
  Files         175      175              
  Lines      127760   127862     +102     
  Branches   127760   127862     +102     
==========================================
+ Hits       113510   113514       +4     
- Misses      11679    11795     +116     
+ Partials     2571     2553      -18     
Flag Coverage Δ
fuzzing 22.10% <36.66%> (+0.35%) ⬆️
tests 88.61% <90.00%> (-0.07%) ⬇️

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.

Copy link
Contributor

@wpaulino wpaulino left a comment

Choose a reason for hiding this comment

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

I think we could make UnfundedV2 channels be resumable as well, currently we don't keep them around after a disconnect

@@ -9107,13 +9107,20 @@ where
"Peer sent shutdown when we needed a channel_reestablish".to_owned(),
));
}
if self.context.channel_state.is_pre_funded_state() {
let mut not_broadcasted =
matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_));
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll also be in this state during splices, so it seems we need more context before attempting to consider it "not broadcasted"

Copy link
Contributor

Choose a reason for hiding this comment

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

If we disconnect during this state with a splice, we need to go back to ChannelReady

Copy link
Collaborator Author

@TheBlueMatt TheBlueMatt Aug 19, 2025

Choose a reason for hiding this comment

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

Probably that just implies that we should not convert to NegotiatingFunding during splices? We have too many things that depend on the state and it seems weird to jump back to the "pre-funding" state when we're not actually pre-funding.

Copy link
Contributor

Choose a reason for hiding this comment

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

We have too many things that depend on the state

How does the same not apply to us being in FundingNegotiated?

it seems weird to jump back to the "pre-funding" state when we're not actually pre-funding

Wouldn't it be better to think about it in terms of whether the channel can be resumed after a disconnect/restart instead of being funded or not? That seems to better align with some of the changes here. I see "pre-funding" more ambiguous now that a v2 channel could be considered pre-funding both before the funding transaction is built and before signatures are exchanged for it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

How does the same not apply to us being in FundingNegotiated?

I meant that we have a lot of things that look at state, and previously FundingNegotiated was part of the "this is pre-funding" and now its sometimes not pre-funding, which is a really weird change to me?

Wouldn't it be better to think about it in terms of whether the channel can be resumed after a disconnect/restart instead of being funded or not?

But it can be resumed, just not the splice itself. I think that's kinda my point here - it feels way more like a post-funding channel that is just paused (quiescent).

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant that we have a lot of things that look at state, and previously FundingNegotiated was part of the "this is pre-funding" and now its sometimes not pre-funding, which is a really weird change to me?

I'm confused then, you think it's a weird change but we're doing it anyway and somehow excluding NegotiatingFunding from following the same pattern?

But it can be resumed, just not the splice itself. I think that's kinda my point here - it feels way more like a post-funding channel that is just paused (quiescent).

Then shouldn't we be relying on whether we're post-funding based on the ChannelPhase and not the ChannelState? ChannelState predates all of this so it made sense back then but not anymore. Why should ChannelState maintain its logical ordering when ChannelPhase has kind of fulfilled that role already?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Then shouldn't we be relying on whether we're post-funding based on the ChannelPhase and not the ChannelState? ChannelState predates all of this so it made sense back then but not anymore. Why should ChannelState maintain its logical ordering when ChannelPhase has kind of fulfilled that role already?

Last discussion I remember we kinda decided ChannelPhase doesn't work and we wanted to get rid of it. Its by no means set, but ChannelPhase has really turned out to be incredibly cumbersome and I'm kinda skeptical of trying to invest in it further. ChannelState has, at least up until the change you're describing here, been totally well-ordered with flags representing the non-ordered stuff.

@jkczyz may have an opinion here too.

Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding ChannelPhase, I don't think we wanted to remove it entirely but rather make it an internal detail of Channel, which it now is. The structs held by the phases are valuable to expose as they restrict which methods can be called and what state is available (e.g., splice operations and pending splices are only available on FundedChannel).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Regarding ChannelPhase, I don't think we wanted to remove it entirely but rather make it an internal detail of Channel, which it now is.

No its not? channelmanager.rs has a ton of "convert state check" nonsense all over the place, with a lot of associated error handling that belongs in channel.rs/Channel.

The structs held by the phases are valuable to expose as they restrict which methods can be called and what state is available (e.g., splice operations and pending splices are only available on FundedChannel).

I'm really unconvinced by this now that we have it. We went down the same path with the interactive transaction builder, first making it an API that channel.rs would access directly, then realizing its actually really not adding much and wrapping it so that channel.rs can always access methods and failing them inside the interactive transaction builder, resulting in a bunch more code there without any real value (you still have to handle the error case of "we received a message for a state it cannot apply to", you just move around where that handling is, in this case to a worse place!).

A state-transition enum makes a lot of sense if there's really a lot of fields that are only exist in some states and that can result in removing a lot of unwraps to access fields that are guaranteed to be set in a state but not available before it. We don't have that with Channel, we have nearly everything in ChannelContext, and still have all the unwraps. Worse, its not like we even have different things in the scope structs - PendingV2Channel has a FundingScope just like FundedChannel (probably implying it should be in the global context), has an interactive signing session (just like FundedChannel, again maybe it should be in the global context...) and really there's barely any difference at all.

Worse still, we have two separate concepts of the "channel's state" - the ChannelState enum, which is granular and useful, and the struct-level thing which is not granular, doesn't save unwraps, and now because its duplicated state can represent a lot of invalid cases where the states conflict. Now, that's not to say that the state enum thing can't be useful - we could actually invest in it, basically removing the top level ChannelState enum, moving those to different structs in the top-level Channel enum, and moving the flags inside of them, allowing, for example, the removal of the ChannelContext in the pre-funded states and making lots of Options in the ChannelContext required. Or we could walk it back and just use the ChannelState everywhere robustly.

At the end of the day, the only unacceptable option is the state were in right now where we have two concepts of state that are very different and don't use either in a robust or reliable way.

Copy link
Contributor

Choose a reason for hiding this comment

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

No its not? channelmanager.rs has a ton of "convert state check" nonsense all over the place, with a lot of associated error handling that belongs in channel.rs/Channel.

Right, my point was the matches on ChannelPhase were removed and in some cases pushed into Channel. In other cases, we do something like as_funded.

I'm really unconvinced by this now that we have it. We went down the same path with the interactive transaction builder, first making it an API that channel.rs would access directly, then realizing its actually really not adding much and wrapping it so that channel.rs can always access methods and failing them inside the interactive transaction builder, resulting in a bunch more code there without any real value (you still have to handle the error case of "we received a message for a state it cannot apply to", you just move around where that handling is, in this case to a worse place!).

Was referring mainly to uses of as_funded, et al. There the checks when receiving a message remain in channelmanager.rs.

A state-transition enum makes a lot of sense if there's really a lot of fields that are only exist in some states and that can result in removing a lot of unwraps to access fields that are guaranteed to be set in a state but not available before it. We don't have that with Channel, we have nearly everything in ChannelContext, and still have all the unwraps. Worse, its not like we even have different things in the scope structs - PendingV2Channel has a FundingScope just like FundedChannel (probably implying it should be in the global context), has an interactive signing session (just like FundedChannel, again maybe it should be in the global context...) and really there's barely any difference at all.

Worse still, we have two separate concepts of the "channel's state" - the ChannelState enum, which is granular and useful, and the struct-level thing which is not granular, doesn't save unwraps, and now because its duplicated state can represent a lot of invalid cases where the states conflict. Now, that's not to say that the state enum thing can't be useful - we could actually invest in it, basically removing the top level ChannelState enum, moving those to different structs in the top-level Channel enum, and moving the flags inside of them, allowing, for example, the removal of the ChannelContext in the pre-funded states and making lots of Options in the ChannelContext required. Or we could walk it back and just use the ChannelState everywhere robustly.

FWIW, we don't have a top-level Channel enum anymore. We have a Channel struct containing a ChannelPhase enum. Totally, agree about your points about invalid cases and making fewer Options in ChannelContext.

Currently, there are a ton of places in channelmaner.rs that want ChannelContext regardless of the phase. So we'd need to tackle untangling that. Additionally, each channel sub-struct (e.g., FundedChannel) typically wants to access some field or method on ChannelContext.

At the end of the day, the only unacceptable option is the state were in right now where we have two concepts of state that are very different and don't use either in a robust or reliable way.

Yeah, I tend to agree. Let's discuss offline.

ChannelState::NegotiatingFunding(_) => true,
ChannelState::FundingNegotiated(flags) => !flags.is_interactive_signing(),
_ => false,
ChannelState::NegotiatingFunding(_) => false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here regarding splices

@wpaulino wpaulino requested review from jkczyz and removed request for joostjager August 18, 2025 22:56
@TheBlueMatt TheBlueMatt added this to the 0.2 milestone Aug 19, 2025
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @jkczyz! 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.

@TheBlueMatt
Copy link
Collaborator Author

Discussed this offline yesterday and I believe we concluded this is probably the right way to go (keeping the ChannelState as Funded during splicing) but @wpaulino was gonna look into it a bit more. In the mean time, I kinda assume this PR can go ahead, and we can change the checks here when splicing actually starts updating ChannelState (as with all the other places that will need to change).

@TheBlueMatt TheBlueMatt requested a review from wpaulino August 21, 2025 14:51
@TheBlueMatt TheBlueMatt self-assigned this Aug 21, 2025
@TheBlueMatt TheBlueMatt added the weekly goal Someone wants to land this this week label Aug 21, 2025
@ldk-reviews-bot
Copy link

🔔 2nd Reminder

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

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @jkczyz @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
weekly goal Someone wants to land this this week
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

4 participants