Skip to content

[Splicing] Tx negotiation during splicing #3736

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

optout21
Copy link
Contributor

@optout21 optout21 commented Apr 15, 2025

Implementation of transaction negotiation during splicing.
Builds on 3407 and 3443.

  • No new phase, Funded(FundedChannel) is used throughout splicing
  • Both FundedChannel and PendingV2Channel can act as a transaction constructor
  • PendingV2Channel logic is put behind a trait -- FundingTxConstructorV2
  • A RenegotiatingScope is used to store extra state during splicing
  • FundingChannel can act as a FundingTxConstructorV2, using the state from RenegotiatingScope (if present)
  • Since both FundedChannel and FundingTxConstructor has context(), context accessors are extracted into a common base trait, ChannelContextProvider (it is also shared by InitialRemoteCommitmentReceiver).

(Also relevant: #3444)

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Apr 15, 2025

👋 I see @wpaulino was un-assigned.
If you'd like another reviewer assignment, please click here.

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

1 similar comment
@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.

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

1 similar comment
@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

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

1 similar comment
@ldk-reviews-bot
Copy link

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

🔔 4th 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.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 4th 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

🔔 5th 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.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 5th 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

🔔 6th 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.

1 similar comment
@ldk-reviews-bot
Copy link

🔔 6th 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.

Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Sorry about the late review. We were traveling to an off site last week. Just a high-level pass on the first four commits. Will need to take a closer look at the last one.

Copy link

codecov bot commented Apr 30, 2025

Codecov Report

❌ Patch coverage is 72.87449% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.99%. Comparing base (7ec13dc) to head (4eb980e).

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 67.36% 31 Missing ⚠️
lightning/src/ln/channelmanager.rs 39.53% 20 Missing and 6 partials ⚠️
lightning/src/ln/interactivetxs.rs 90.82% 9 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3736      +/-   ##
==========================================
+ Coverage   88.92%   88.99%   +0.07%     
==========================================
  Files         174      174              
  Lines      123869   123793      -76     
  Branches   123869   123793      -76     
==========================================
+ Hits       110152   110174      +22     
+ Misses      11256    11164      -92     
+ Partials     2461     2455       -6     
Flag Coverage Δ
fuzzing 22.21% <0.00%> (+0.03%) ⬆️
tests 88.82% <72.87%> (+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.

@optout21 optout21 force-pushed the splice-dual-tx4 branch 3 times, most recently from 88d2e83 to 866368d Compare May 5, 2025 11:59
@optout21
Copy link
Contributor Author

optout21 commented May 6, 2025

Ready for a new round of review. I have addressed the comments, applied most of them. There is still one to-do (update channel reserve values), that I will do, but the rest is ready for review.
I did the changes in separate 'fix' commits.

@optout21
Copy link
Contributor Author

Ready for a new round of review. All pending and newly raised comments addressed.

@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from 6cb8084 to aeef39d Compare July 16, 2025 22:12
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Taking over this from @optout21 so that he can concentrate on #3641.

fn begin_interactive_funding_tx_construction<ES: Deref>(
&mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey,
change_destination_opt: Option<ScriptBuf>,
is_initiator: bool, change_destination_opt: Option<ScriptBuf>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Updated to take FundingNegotiationContext with an added is_initiator field. Left it in interactivetxs.rs for the time being given it's tested there, too. Can move it at the end of the review.

fn begin_interactive_funding_tx_construction<ES: Deref>(
&mut self, signer_provider: &SP, entropy_source: &ES, holder_node_id: PublicKey,
change_destination_opt: Option<ScriptBuf>,
is_initiator: bool, change_destination_opt: Option<ScriptBuf>,
prev_funding_input: Option<(TxIn, TransactionU16LenLimited)>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Rebased. Now prev_funding_input is shared_funding_input passed as Option<SharedOwnedInput>.


// TODO(splicing): Add prev funding tx as input, must be provided as a parameter
if is_initiator {
if let Some(prev_funding_input) = prev_funding_input {
Copy link
Contributor

Choose a reason for hiding this comment

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

Added a fixup for the shared input.

#[cfg(splicing)]
fn as_renegotiating_channel(&mut self) -> Result<NegotiatingChannelView<SP>, &'static str> {
if let Some(ref mut pending_splice) = &mut self.pending_splice {
if let Some(ref mut funding) = &mut pending_splice.funding {
Copy link
Contributor

Choose a reason for hiding this comment

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

But we need the FundingScope and PendingSplice to initialize NegotiatingChannelView, right?

Err(tx_msg_str) => return Err(MsgHandleErrInternal::from_chan_no_close(ChannelError::Warn(
format!("Got a {tx_msg_str} message with no interactive transaction construction expected or in-progress")
), channel_id)),
Err(err) => return Err(MsgHandleErrInternal::from_chan_no_close(err, channel_id)),
Copy link
Contributor

Choose a reason for hiding this comment

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

We can't add it here. Instead, we would need to do it at each call site of as_negotiating_channel s (i.e. within each HandleTxMsgFn passed here and wherever else it is called).

Comment on lines 2790 to 2908
interactive_tx_constructor: &'a mut Option<InteractiveTxConstructor>,
interactive_tx_signing_session: &'a mut Option<InteractiveTxSigningSession>,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need to track interactive_tx_signing_session at all here, as long as we separately set the session in funding_tx_constructed.

This is called from Channel::funding_tx_constructed, so we wouldn't know where to set it on without

interactive_tx_constructor also shouldn't be an Option, but it seems to be that way right now because of begin_interactive_funding_tx_construction and funding_tx_constructed.

For begin_interactive_funding_tx_construction, maybe it makes more sense to make it a standalone function (no longer a member on NegotiatingChannelView) that simply returns an InteractiveTxConstructor that we can set on the channel separately.

We need it in the next commit when calling tx_add_input, et al.

) -> Result<Option<InteractiveTxMessageSend>, AbortReason>
where ES::Target: EntropySource
{
debug_assert!(matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_)));
if self.is_splice {
debug_assert!(matches!(self.context.channel_state, ChannelState::ChannelReady(_)));
Copy link
Contributor

Choose a reason for hiding this comment

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

Was there any resolution on this?

@jkczyz jkczyz requested a review from wpaulino July 16, 2025 22:13
@optout21
Copy link
Contributor Author

Rebased to current main, including the relevant #3842 .

I rebased, but still haven't actually used the shared input. I can see that's now done in begin_interactive_funding_tx_construction (using to_splice_funding_input)! 👍

@optout21
Copy link
Contributor Author

The splicing test fails, but when properly setting the parameters for calculate_change_output_value, it passes. 😎
https://github.com/lightningdevkit/rust-lightning/pull/3736/files#r2212276344

@jkczyz
Copy link
Contributor

jkczyz commented Jul 18, 2025

The splicing test fails, but when properly setting the parameters for calculate_change_output_value, it passes. 😎 https://github.com/lightningdevkit/rust-lightning/pull/3736/files#r2212276344

Thanks @optout21! That did fix the test though a bunch of assertions that had commented out marked FIXME still fail after uncommenting. Let me know if anything jumps out.

@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from aeef39d to 05a6c85 Compare July 18, 2025 18:18
@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.

@optout21
Copy link
Contributor Author

optout21 commented Jul 19, 2025

Thanks @optout21! That did fix the test though a bunch of assertions that had commented out marked FIXME still fail after uncommenting. Let me know if anything jumps out.

Pushed a commit to fix the splice test. 3b31240
The order of the outputs may change (fixed for a given code version, but may change with some code change).
The order of the inputs is non-deterministic (may change at every test run).
I extended the test code to be able to handle either order.
An exact amount also had to be adjusted, I'm not sure why, but fee computation changed somewhat (14146 vs 14094).

@ldk-reviews-bot
Copy link

🔔 2nd 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.

@wpaulino wpaulino removed their request for review July 21, 2025 23:04
@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from 3b31240 to d400a8b Compare July 22, 2025 04:42
Copy link
Contributor

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Squashed all the fixups and created some standalone commits where possible. Will need to update some of the commit authorship. Hopefully, this will be a little easier to review now.

@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from d400a8b to 07190d1 Compare July 22, 2025 17:16
@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from f3a3877 to 575990a Compare July 22, 2025 23:42
@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from 575990a to 1cbfc55 Compare July 23, 2025 22:57
let post_value_to_self_msat = AddSigned::checked_add_signed(
prev_funding.value_to_self_msat,
our_funding_contribution_sats * 1000,
);
debug_assert!(post_value_to_self_msat.is_some());
let post_value_to_self_msat = post_value_to_self_msat.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

The debug_assert!(post_value_to_self_msat.is_some()) assumes that adding our_funding_contribution_sats * 1000 to prev_funding.value_to_self_msat will never overflow. However, this isn't guaranteed, especially with large values.

If the addition overflows, post_value_to_self_msat will be None, and the subsequent unwrap() will panic. Consider handling the overflow case explicitly, perhaps by capping the value at u64::MAX or returning an error if the addition would overflow. This would make the code more robust against edge cases.

Spotted by Diamond

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

Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't be possible so long as our_funding_contribution_sats doesn't exceed the total bitcoin supply. I will add more checks in the splice-out follow-up PR.

@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from 1cbfc55 to 2eeb134 Compare July 24, 2025 19:31
optout21 and others added 5 commits July 28, 2025 12:18
This is a simple rename, DualFundingContext to FundingNegotiationContext,
to suggest that this is use not only in dual-funded channel open.
Also rename the field dual_funding_context to funding_negotiation_context.
PendingSplice holds a FundingScope being negotiated. However, when
implementing funding negotiation, other states are possible depending on
which party initiated the splice. Using an enum prevents needing various
Option fields which may result in invalid states.

When the user initiates the splice, the FundingNegotiationContext must
be held until the counterparty responds with splice_ack. At that point
enough information becomes available to create a new FundingScope and an
InteractiveTxConstructor.

When the counterparty initiates the splice, both a new FundingScope and
an InteractiveTxConstructor can be created immediately when responding
with splice_ack.

After the transaction is constructed, those are no longer needed. At
that point an InteractiveTxSigningSession is tracked until signatures
are exchanged.
FundingNegotiationContext and PendingSplice both hold the user's
contribution to a splice, which doesn't need to be duplicated. Instead,
only store this in FundingNegotiationContext, which then can be used to
create an InteractiveTxConstructor when transitioning to
FundingNegotiation::Pending.

This commit updates that code to properly compute change outputs using
the FundingNegotiationContext by not considering the shared input since
it is accounted for in the shared output.

Co-authored-by: Wilmer Paulino <[email protected]>
Co-authored-by: Jeffrey Czyz <[email protected]>
InteractiveTxConstructor was only used in PendingV2Channel methods, but
for splicing those methods are needed for FundedChannel, too. Refactor
the code such that each type has a method for accessing its
InteractiveTxConstructor such that it can be called in either use,
refactoring code out of PendingV2Channel as needed.

Co-authored-by: Wilmer Paulino <[email protected]>
Co-authored-by: Jeffrey Czyz <[email protected]>
Update splice_channel, split_init, and splice_ack to implement
transitioning from splice initialization to funding transaction
negotiation.

Co-authored-by: optout <[email protected]>
Co-authored-by: Jeffrey Czyz <[email protected]>
@jkczyz jkczyz force-pushed the splice-dual-tx4 branch from 2eeb134 to 4eb980e Compare July 28, 2025 17:32
Comment on lines +5501 to +5507
if is_splice {
let message = "TODO Forced error, incomplete implementation".to_owned();
// TODO(splicing) Forced error, as the use case is not complete
return Err(ChannelError::Close((
message.clone(),
ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false), message }
)));
Copy link
Contributor

Choose a reason for hiding this comment

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

When returning an error in the splice case, the function should reset funding.channel_transaction_parameters.funding_outpoint to None to maintain consistency with the non-splice error handling path at line 5515. This prevents leaving the channel in an inconsistent state if an error occurs during transaction construction.

Suggested change
if is_splice {
let message = "TODO Forced error, incomplete implementation".to_owned();
// TODO(splicing) Forced error, as the use case is not complete
return Err(ChannelError::Close((
message.clone(),
ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false), message }
)));
if is_splice {
let message = "TODO Forced error, incomplete implementation".to_owned();
// TODO(splicing) Forced error, as the use case is not complete
funding.channel_transaction_parameters.funding_outpoint = None;
return Err(ChannelError::Close((
message.clone(),
ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false), message }
)));

Spotted by Diamond

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

Copy link
Contributor

Choose a reason for hiding this comment

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

@wpaulino I see that the call site in ChannelManager maps any error returned using MsgHandleErrInternal::send_err_msg_no_close. Doesn't that mean whenever we return ChannelError::Warn in Channel::funding_tx_constructed we'll send an error instead of a warning message?

More generally, do we want to distinguish between what is done for a splice vs v2-establishment? Presumably we don't want to close the channel for the former but would for the latter?

Copy link
Contributor

Choose a reason for hiding this comment

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

Correct, we should fix that

@@ -2934,7 +2852,7 @@ where
where
L::Target: Logger
{
let our_funding_satoshis = self.funding_negotiation_context.our_funding_satoshis;
let our_funding_satoshis = self.funding_negotiation_context.our_funding_contribution_satoshis;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: move this down to where it's actually needed

fn into_interactive_tx_constructor<SP: Deref, ES: Deref>(
self, context: &ChannelContext<SP>, funding: &FundingScope, signer_provider: &SP,
entropy_source: &ES, holder_node_id: PublicKey, change_destination_opt: Option<ScriptBuf>,
shared_funding_input: Option<SharedOwnedInput>,
Copy link
Contributor

Choose a reason for hiding this comment

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

We can/should probably track this in FundingNegotiationContext

#[cfg(splicing)]
fn into_interactive_tx_constructor<SP: Deref, ES: Deref>(
self, context: &ChannelContext<SP>, funding: &FundingScope, signer_provider: &SP,
entropy_source: &ES, holder_node_id: PublicKey, change_destination_opt: Option<ScriptBuf>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Once we support splice out, we'll want to make the current our_funding_inputs an enum, where the variants are:

  • SpliceIn { inputs: Vec<(TxIn, TransactionU16LenLimited)>, change_script: Option<ScriptBuf> }
  • SpliceOut { outputs: Vec<TxOut> }
  • Mixed { inputs: Vec<(TxIn, TransactionU16LenLimited)>, outputs: Vec<TxOut> }

if self.our_funding_contribution_satoshis > 0 {
let change_value_opt = calculate_change_output_value(
&self,
funding.channel_transaction_parameters.splice_parent_funding_txid.is_some(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's assert somewhere that when this is set, shared_funding_input also is


if shared_input.is_some() {
if is_splice {
// FIXME: Needs to consider different weights based on channel type
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO(splicing) if we're not going to fix it here

},
};
if let Some(msg_send_event) = msg_send_event_opt {
peer_state.pending_msg_events.push(msg_send_event);
};
if let Some(signing_session) = signing_session_opt {
if ready_to_sign {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thoughts on naming this negotiation_complete or funding_tx_complete instead?

let input = TxIn {
previous_output: funding_txo.into_bitcoin_outpoint(),
script_sig: ScriptBuf::new(),
sequence: Sequence::ZERO,
Copy link
Contributor

Choose a reason for hiding this comment

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

We may want to check what other implementations are using here

})?;
let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message();

debug_assert!(self.interactive_tx_signing_session.is_none());
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be some as we're the splice initiator

signer_provider,
entropy_source,
holder_node_id.clone(),
None,
Copy link
Contributor

Choose a reason for hiding this comment

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

There should be an optional change address set here

Comment on lines +5501 to +5507
if is_splice {
let message = "TODO Forced error, incomplete implementation".to_owned();
// TODO(splicing) Forced error, as the use case is not complete
return Err(ChannelError::Close((
message.clone(),
ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(false), message }
)));
Copy link
Contributor

Choose a reason for hiding this comment

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

Correct, we should fix that

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.

4 participants