Skip to content

Conversation

jkczyz
Copy link
Contributor

@jkczyz jkczyz commented Sep 17, 2025

When a splice has been negotiated, it remains pending until the funding transaction has been confirmed and locked by both sides. Emit an Event::SplicePending when it reaches this state. At this point, the inputs used for the splice cannot be reused except for an RBF attempt. Once the splice is locked, an Event::DiscardFunding will be emitted for any unsuccessful candidates.

Similarly, a splice may fail before a splice has finished negotiation for various reasons. Emit an Event::SpliceFailed in these cases so the user may reuse the inputs.

The commits were generated by Claude Code, so leaving the PR in draft for now.

TODO:

  • Populate Event::SpliceFailed with inputs and outputs.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Sep 17, 2025

🎉 This PR is now ready for review!
Please choose at least one reviewer by assigning them on the right bar.
If no reviewers are assigned within 10 minutes, I'll automatically assign one.
Once the first reviewer has submitted a review, a second will be assigned if required.

@jkczyz
Copy link
Contributor Author

jkczyz commented Sep 17, 2025

Looking for high-level feedback on the approach and if all scenarios were covered. Specifically, is SpliceFailed sufficient to let a user know if an input can be reused? Seems for an RBF attempt they could have used an input that was used by a previous attempt that has had a SplicePending emitted. So it could be reused for another RBF attempt, IIUC, but not for splicing another channel, for instance.

Copy link

codecov bot commented Sep 17, 2025

Codecov Report

❌ Patch coverage is 10.46512% with 308 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.68%. Comparing base (3564646) to head (0db24f3).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 12.50% 150 Missing and 4 partials ⚠️
lightning/src/ln/channelmanager.rs 11.02% 107 Missing and 6 partials ⚠️
lightning/src/events/mod.rs 0.00% 41 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4077      +/-   ##
==========================================
- Coverage   87.81%   87.68%   -0.13%     
==========================================
  Files         176      176              
  Lines      131770   132034     +264     
  Branches   131770   132034     +264     
==========================================
+ Hits       115719   115780      +61     
- Misses      13416    13615     +199     
- Partials     2635     2639       +4     
Flag Coverage Δ
fuzzing 21.52% <2.61%> (-0.08%) ⬇️
tests 87.52% <10.46%> (-0.13%) ⬇️

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.

@jkczyz jkczyz force-pushed the 2025-09-splice-events branch from 0db24f3 to 41c1e60 Compare September 18, 2025 02:03
Comment on lines 11890 to 12031
let splice_funding = self.validate_splice_ack(msg)?;
let splice_funding = self.validate_splice_ack(msg).map_err(|err| {
let splice_failed = SpliceFundingFailed {
channel_id: self.context.channel_id,
counterparty_node_id: self.context.counterparty_node_id,
user_channel_id: self.context.user_id,
funding_txo: None,
channel_type: None,
contributed_inputs: Vec::new(),
contributed_outputs: Vec::new(),
};
(err, splice_failed)
})?;
Copy link
Contributor Author

@jkczyz jkczyz Sep 18, 2025

Choose a reason for hiding this comment

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

This commit will likely need to be re-worked to have validate_splice_ack return Option<SpliceFundingFailed> as part of its error. But it seems we never clear pending_splice.funding_negotiation here.

@wpaulino Should we? I'd imagine if we ever emit Event::SpliceFailed then pending_splice.funding_negotiation should no longer be set.

Comment on lines 11920 to 12074
ChannelError::WarnAndDisconnect(format!(
let splice_failed = SpliceFundingFailed {
channel_id: self.context.channel_id,
counterparty_node_id: self.context.counterparty_node_id,
user_channel_id: self.context.user_id,
funding_txo: splice_funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()),
channel_type: Some(splice_funding.get_channel_type().clone()),
contributed_inputs: Vec::new(),
contributed_outputs: Vec::new(),
};
let channel_error = ChannelError::WarnAndDisconnect(format!(
"Failed to start interactive transaction construction, {:?}",
err
))
));
(channel_error, splice_failed)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Likewise here.

Comment on lines +2632 to 2653
fn is_initiator(&self) -> bool {
match self {
FundingNegotiation::AwaitingAck { context } => context.is_initiator,
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.is_initiator()
},
FundingNegotiation::AwaitingSignatures { is_initiator, .. } => *is_initiator,
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if there is a preferred way to go about this. It's nice being able to filter like is done in this commit and later fixups:

.filter(|funding_negotiation| funding_negotiation.is_initiator())

But it requires tracking is_initiator both in InteractiveTxConstructor and FundingNegotiation::AwaitingSignatures, at least as currently written.


pub(super) struct InteractiveTxConstructor {
state_machine: StateMachine,
is_initiator: bool,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Might be able to get this from the StateMachine, but not if it transitioned to NegotiationAborted, it seems.

Comment on lines 1777 to 1778
contributed_inputs: Vec::new(),
contributed_outputs: Vec::new(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

One issue with populating these is if we are in FundingNegotiation::AwaitingSignatures, then we'd need to get them from ChannelContext::interactive_tx_signing_session, which may not have been set yet in FundedChannel::funding_tx_constructed depending on the error.

Comment on lines 2004 to 2018
let signing_session = interactive_tx_constructor.into_signing_session();
let commitment_signed = chan.context.funding_tx_constructed(
let commitment_signed_result = chan.context.funding_tx_constructed(
&mut funding,
signing_session,
true,
chan.holder_commitment_point.next_transaction_number(),
&&logger,
)?;
);

// This must be set even if returning an Err. Otherwise,
// fail_interactive_tx_negotiation won't produce a SpliceFailed event.
pending_splice.funding_negotiation =
Some(FundingNegotiation::AwaitingSignatures { funding });

return Ok(commitment_signed);
return commitment_signed_result;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Here, specifically, we may not have set ChannelContext::interactive_tx_signing_session for some errors.

@ldk-reviews-bot
Copy link

🔔 1st Reminder

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

1 similar comment
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @TheBlueMatt @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

🔔 2nd Reminder

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

1 similar comment
@ldk-reviews-bot
Copy link

🔔 2nd Reminder

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

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.

What's the status of this? IMO we should land it early so we can write tests and discover where we're missing coverage.


/// Information about a splice funding negotiation that has been completed.
/// This is returned from channel operations and converted to an Event::SplicePending in ChannelManager.
pub struct SpliceFundingNegotiated {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Meh, just return the 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.

Hmmm... our pattern has been to only create events in ChannelManager. Though we can drop the first three fields and take them from the ChannelContext in ChannelManager as is done for other events.

/// Information about a splice funding negotiation that has been completed.
/// This is returned from channel operations and converted to an Event::SplicePending in ChannelManager.
pub struct SpliceFundingNegotiated {
/// The channel_id of the channel being spliced.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lol, these comments are great, they're exactly what I expect from an intern, they say absolutely nothing but meet the not-even-a-requirement for things having docs.

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... I'll take a pass and clean up Claude's work. Didn't really give it much prompting to be honest.

counterparty_node_id: PublicKey,
/// The outpoint of the channel's splice funding transaction.
funding_txo: OutPoint,
/// The features that this channel will operate with.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should clarify that while it won't currently change this may change with future splices.

/// The outpoint of the channel's splice funding transaction, if one was created.
funding_txo: Option<OutPoint>,
/// The features that this channel will operate with, if available.
channel_type: Option<ChannelTypeFeatures>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure we need this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fine to drop it here is you prefer. Keeping it in SplicePending since it can change from the original funding.

/// Input outpoints contributed to the splice transaction.
contributed_inputs: Vec<OutPoint>,
/// Outputs contributed to the splice transaction.
contributed_outputs: Vec<TxOut>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Indeed, I believe we discussed on the call last week, but we should include information about other splices still pending.

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, the idea was to only include inputs that can be unlocked. I need to update this still.

@jkczyz
Copy link
Contributor Author

jkczyz commented Sep 23, 2025

What's the status of this? IMO we should land it early so we can write tests and discover where we're missing coverage.

This depends on #4097 now.

@ldk-reviews-bot
Copy link

🔔 3rd 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.

@jkczyz jkczyz force-pushed the 2025-09-splice-events branch from 41c1e60 to a01b467 Compare September 24, 2025 16:38
@jkczyz jkczyz marked this pull request as ready for review September 24, 2025 16:39
Comment on lines 1915 to 1910
let splice_funding_failed = funding_negotiation_opt
.filter(|funding_negotiation| funding_negotiation.is_initiator())
.map(|funding_negotiation| {
// Create SpliceFundingFailed for the aborted splice
let (funding_txo, channel_type) = match &funding_negotiation {
FundingNegotiation::ConstructingTransaction { funding, .. } => {
(funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()), Some(funding.get_channel_type().clone()))
},
FundingNegotiation::AwaitingSignatures { funding, .. } => {
(funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()), Some(funding.get_channel_type().clone()))
},
FundingNegotiation::AwaitingAck { .. } => {
(None, None)
},
};

SpliceFundingFailed {
channel_id: funded_channel.context.channel_id,
counterparty_node_id: funded_channel.context.counterparty_node_id,
user_channel_id: funded_channel.context.user_id,
funding_txo,
channel_type,
contributed_inputs: Vec::new(),
contributed_outputs: Vec::new(),
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Bug: In tx_abort handling for funded channels, when a splice negotiation is aborted during the ConstructingTransaction state, the contributed inputs and outputs are not extracted from the interactive_tx_constructor before creating the SpliceFundingFailed event. The code sets contributed_inputs and contributed_outputs to empty vectors, but if the local node had already contributed inputs/outputs to the transaction construction, this information should be preserved so users know which UTXOs they can reuse. The interactive_tx_constructor contains inputs_to_contribute and outputs_to_contribute fields that should be extracted before the constructor is dropped.

Suggested change
let splice_funding_failed = funding_negotiation_opt
.filter(|funding_negotiation| funding_negotiation.is_initiator())
.map(|funding_negotiation| {
// Create SpliceFundingFailed for the aborted splice
let (funding_txo, channel_type) = match &funding_negotiation {
FundingNegotiation::ConstructingTransaction { funding, .. } => {
(funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()), Some(funding.get_channel_type().clone()))
},
FundingNegotiation::AwaitingSignatures { funding, .. } => {
(funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()), Some(funding.get_channel_type().clone()))
},
FundingNegotiation::AwaitingAck { .. } => {
(None, None)
},
};
SpliceFundingFailed {
channel_id: funded_channel.context.channel_id,
counterparty_node_id: funded_channel.context.counterparty_node_id,
user_channel_id: funded_channel.context.user_id,
funding_txo,
channel_type,
contributed_inputs: Vec::new(),
contributed_outputs: Vec::new(),
}
});
let splice_funding_failed = funding_negotiation_opt
.filter(|funding_negotiation| funding_negotiation.is_initiator())
.map(|funding_negotiation| {
// Create SpliceFundingFailed for the aborted splice
let (funding_txo, channel_type, contributed_inputs, contributed_outputs) = match &funding_negotiation {
FundingNegotiation::ConstructingTransaction { funding, interactive_tx_constructor, .. } => {
(funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()),
Some(funding.get_channel_type().clone()),
interactive_tx_constructor.inputs_to_contribute.clone(),
interactive_tx_constructor.outputs_to_contribute.clone())
},
FundingNegotiation::AwaitingSignatures { funding, .. } => {
(funding.get_funding_txo().map(|txo| txo.into_bitcoin_outpoint()),
Some(funding.get_channel_type().clone()),
Vec::new(),
Vec::new())
},
FundingNegotiation::AwaitingAck { .. } => {
(None, None, Vec::new(), Vec::new())
},
};
SpliceFundingFailed {
channel_id: funded_channel.context.channel_id,
counterparty_node_id: funded_channel.context.counterparty_node_id,
user_channel_id: funded_channel.context.user_id,
funding_txo,
channel_type,
contributed_inputs,
contributed_outputs,
}
});

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@jkczyz
Copy link
Contributor Author

jkczyz commented Sep 24, 2025

I pushed four new commits immediately after the SplicePending commits that allows us to extract inputs / outputs from interactivetx.rs structs for SpliceFailed events. There's now a NegotiationError, which wraps an AbortReason along with the inputs and outputs. There are still a few places where the inputs and outputs need to be populated in channel.rs, though.

@jkczyz
Copy link
Contributor Author

jkczyz commented Sep 24, 2025

Specifically, see the into_negotiation_error methods.

@jkczyz jkczyz force-pushed the 2025-09-splice-events branch from a01b467 to 8ff58b9 Compare September 24, 2025 16:58
An upcoming commit will include the contributed inputs and outputs in
an error whenever ConstructedTransaction::new fails. In order to DRY up
that logic, this commit updates the constructor to create the resulting
object prior to performing any checks. This way a conversion method can
be added that extracts the necessary input and output data.
Both NegotiationContext::validate_tx and ConstructedTransaction::new
contain validity checks. Move the former into the latter in order to
consolidate the checks in a single place. This will also allow for
reusing error construction in an upcoming commit.
Instead of popping each input and output to contribute during an
interactive tx session, clone the necessary parts and keep around the
original inputs and outputs. This will let us reuse them later when
constructing an error. The tradeoff is using additional memory to avoid
more code complexity required to extract the sent input and outputs from
NegotiationContext.
@jkczyz
Copy link
Contributor Author

jkczyz commented Sep 25, 2025

Opened #4123 based on some offline discussion, which this PR is now based on. It includes NegotiationError and related refactoring. However, it makes a more limited change to interactivetx.rs. Instead of adding complexity to the state transition code, it simply maintains the inputs and outputs from InteractiveTxConstructor -- instead of popping them -- so they can be used to form SpliceFailed events.

Currently, only the shared input index is stored in
ConstructedTransaction. This will be used later to filter out the shared
input when constructing an error during interactive tx negotiation.
Store the shared output index as well so that the shared output can be
filtered out as well.
When an interactive tx sessions is aborted, the user will need to be
notified with an event indicating which inputs and outputs were
contributed. This allows them to re-use inputs that are no longer in
use. This commit introduces a NegotiationError type to achieve this,
which includes the inputs and outputs along with an AbortReason.
@jkczyz jkczyz force-pushed the 2025-09-splice-events branch from 3a08a0e to 69a4748 Compare September 25, 2025 02:15
jkczyz and others added 4 commits September 25, 2025 14:48
Once a splice has been negotiated and its funding transaction has been
broadcast, it is considered pending until both parties have seen enough
confirmations to consider the funding locked. Add an event used to
indicate this.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Once a splice has been negotiated and its funding transaction has been
broadcast, emit a SplicePending event. Once this occurs, the inputs
contributed to the splice cannot be reused except by an RBF attempt.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Once a splice has been successfully initiated, but prior to signing any
negotiated funding transaction, it may fail. Add an event used to
indicate this and which UTXOs can be reused.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
When interactive transaction construction fails during splice funding
negotiation, emit Event::SpliceFailed to notify users of the failure.

This introduces a SpliceFundingFailed struct to carry failure information
from channel functions to ChannelManager, where it is converted to the
appropriate event. The struct includes channel metadata and placeholders
for contributed inputs/outputs (currently empty pending access to private
InteractiveTxConstructor fields).

Updates all interactive transaction functions (tx_add_input, tx_add_output,
tx_remove_input, tx_remove_output, tx_complete) to return failure
information alongside abort messages, enabling proper event emission
when splice negotiations fail.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
jkczyz and others added 3 commits September 26, 2025 18:58
When a tx_abort message is successfully processed for a funded channel
with an active splice negotiation, emit Event::SpliceFailed to notify
users that the splice operation was aborted by the counterparty.

This extends the SpliceFailed event coverage to handle abort scenarios,
providing comprehensive splice failure notifications across all stages:

- AwaitingAck: funding_txo and channel_type are None since funding
  parameters were not yet established
- ConstructingTransaction/AwaitingSignatures: Include actual funding
  information since negotiation had progressed to funding establishment

The implementation captures splice context before taking the funding
negotiation state, ensuring accurate failure information is available
for event emission while maintaining proper tx_abort acknowledgment
behavior per the Lightning specification.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
…lice negotiations

Adds SpliceFailed event emission immediately after ChannelClosed events when a
FundedChannel is shut down while having an active splice negotiation. This ensures
users are notified when splice operations are terminated due to channel closure.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Since quiescence is terminated upon disconnection, any outstanding
splice negotiation should result in emitting a SpliceFailed event as
long as we haven't reached FundingNegotiation::AwaitingSignatures. This
may occur if we explicitly disconnect the peer (e.g., when failing to
process splice_ack) or if the connection is lost..
@jkczyz jkczyz force-pushed the 2025-09-splice-events branch from 69a4748 to 0f85dd8 Compare September 26, 2025 22:59
@ldk-reviews-bot
Copy link

🔔 4th 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
Status: No status
Development

Successfully merging this pull request may close these issues.

3 participants