Skip to content

Conversation

wpaulino
Copy link
Contributor

An interactive funding construction can be considered failed upon a disconnect or a tx_abort message. So far, we've consumed the InteractiveTxConstructor in the latter case, but not the former. Additionally, we may have splice-specific state that needs to be consumed as well to allow us to negotiate another splice later on.

This commit ensures that we properly consume all splice and interactive funding state whenever possible upon a disconnect or tx_abort.

The interactive funding state is safe to consume as long as we have either yet to reach AwaitingSignatures, or we have but tx_signatures has not been sent/received.

The splice state is safe to consume as long as we don't have a pending FundingNegotiation::AwaitingSignatures with a tx_signatures sent/received and we don't have any negotiated candidates. Note that until splice RBF is supported, it is not currently possible to have any negotiated candidates with a pending interactive funding transaction.

@wpaulino wpaulino added this to the 0.2 milestone Sep 24, 2025
@wpaulino wpaulino requested a review from jkczyz September 24, 2025 19:40
@wpaulino wpaulino self-assigned this Sep 24, 2025
@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Sep 24, 2025

👋 Thanks for assigning @jkczyz 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 Sep 24, 2025

Codecov Report

❌ Patch coverage is 76.78571% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.65%. Comparing base (3e21ba3) to head (a5dcdc1).
⚠️ Report is 72 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 40.90% 51 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4120      +/-   ##
==========================================
- Coverage   88.72%   88.65%   -0.07%     
==========================================
  Files         177      180       +3     
  Lines      133404   135086    +1682     
  Branches   133404   135086    +1682     
==========================================
+ Hits       118365   119767    +1402     
- Misses      12325    12557     +232     
- Partials     2714     2762      +48     
Flag Coverage Δ
fuzzing 21.76% <21.59%> (+0.03%) ⬆️
tests 88.50% <76.78%> (-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.

Comment on lines +6766 to +6781
signing_session.holder_tx_signatures().is_some()
|| signing_session.has_received_tx_signatures()
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it matter if we received their signatures if we haven't sent ours? Or is it because if they sent theirs then they would not have reset their state?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're supposed to remember the channel as soon as one side has sent tx_signatures, because you can't be sure the other side didn't just sign and broadcast without sending their own.

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

.pending_splice
.as_mut()
.and_then(|pending_splice| pending_splice.funding_negotiation.take());
if funded_channel.should_reset_pending_splice_funding_negotiation() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like we aren't calling fail_interactive_tx_negotiation when failing to handle a splice_ack. #4077 updates fail_interactive_tx_negotiation to take a NegotiationError, which it uses to construct a SpliceFundingFailed struct.

For FundingNegotiation::ConstructingTransaction, the NegotiationError is formed by copying the inputs from the InteractiveTxConstructor. For FundingNegotiation::AwaitingAck when handling splice_ack, we'd need to do something similar?

Either way we are cloning the inputs just to later take the FundingNegotiation here. Maybe that is ok for now? Any thoughts on a better way of doing this?

Copy link
Contributor

@jkczyz jkczyz Sep 25, 2025

Choose a reason for hiding this comment

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

Hmmm... so we are also already take'ing the FundingNegotiation in one place in splice_ack handling (when FundingNegotiation::into_interactive_tx_constructor fails), but not earlier when validating the splice_ack message.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We shouldn't need to call fail_interactive_tx_negotiation whenever we fail handling a message by sending a warning and disconnecting.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, so failing to process a splice_ack would just disconnect, and the peer could re-send it after reconnecting. Do we eventually timeout quiescence somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Quiescence is also implicitly terminated upon disconnection, but we do have a timeout enforced at should_disconnect_peer_awaiting_response.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, so we'd need to produce a SpliceFailed event in some other way? I'm thinking when we are in FundingNegotiation::AwaitingAck and fail processing the splice_ack.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah sounds like that logic might need to live in the disconnection handler, since we'll need to emit an event anyway if a peer disconnects mid-splice negotiation for whatever reason.

jkczyz
jkczyz previously approved these changes Sep 26, 2025
An interactive funding construction can be considered failed upon a
disconnect or a `tx_abort` message. So far, we've consumed the
`InteractiveTxConstructor` in the latter case, but not the former.
Additionally, we may have splice-specific state that needs to be
consumed as well to allow us to negotiate another splice later on.

This commit ensures that we properly consume all splice and interactive
funding state whenever possible upon a disconnect or `tx_abort`.

The interactive funding state is safe to consume as long as we have
either yet to reach `AwaitingSignatures`, or we have but `tx_signatures`
has not been sent/received. In all of these cases, we also make sure to
clear the quiescent state flag such that we're able to resume processing
updates on the channel.

The splice state is safe to consume as long as we don't have a pending
`FundingNegotiation::AwaitingSignatures` with a `tx_signatures`
sent/received and we don't have any negotiated candidates. Note that
until splice RBF is supported, it is not currently possible to have any
negotiated candidates with a pending interactive funding transaction.
Comment on lines +6780 to +6781
signing_session.holder_tx_signatures().is_some()
|| signing_session.has_received_tx_signatures()
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be negated? If either side has exchanged signatures then we don't want to reset?

Comment on lines +1698 to +1702
funded_channel.context.channel_state.clear_quiescent();
funded_channel
.pending_splice
.as_mut()
.and_then(|pending_splice| pending_splice.funding_negotiation.take());
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 also clear interactive_tx_signing_session if in FundingNegotiation::AwaitingSignatures and we haven't sent/received signatures?

}
}

if self.should_reset_pending_splice_funding_negotiation().unwrap_or(false) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why use unwrap_or(false) here but fail_interactive_tx_negotiation uses true?

if self.pending_splice.is_none() {
// If we were in quiescence but a splice was never negotiated, we shouldn't be
// quiescent anymore upon reconnecting.
self.context.channel_state.clear_quiescent();
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we always want to clear quiescence upon disconnect?

Comment on lines +8930 to +8932
self.pending_splice
.as_mut()
.and_then(|pending_splice| pending_splice.funding_negotiation.take());
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, I have this in a different function in #4077 since I need to funding_negotiation to produce a SpliceFailed, but I should be able to move it there.

Comment on lines +1846 to +1848
if funded_channel.should_reset_pending_splice_state() {
funded_channel.pending_splice.take();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

should_reset_pending_splice_state calls should_reset_pending_splice_funding_negotiation again, but the latter is partly determined based on pending_splice.funding_negotiation, which is taken above. Everything seems to check out for the second call based on how unwrap_or is used, but that feels kinda fragile.

Wondering if instead of using two different methods, we could do something like:

fn maybe_reset_splice_funding_negotation(&mut self) -> Option<SpliceFundingFailed> {
    // ...
}

This would clear pending_splice.funding_negotiation, pending_splice, and interactive_tx_signing_session, as appropriate and convert them to the SpliceFundingFailed needed to produce a SpliceFailed event.

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.

4 participants