Skip to content

Exchange splice_locked messages #3741

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

Conversation

jkczyz
Copy link
Contributor

@jkczyz jkczyz commented Apr 16, 2025

After a splice has been negotiated, each party must send a splice_locked message to the other party once the splice transaction has had an acceptable number of confirmations. Update the logic for processing newly confirmed transactions and updated best block to send splice_locked when appropriate.

Likewise, handle splice_locked and promote the channel's FundingScope once both splice_locked messages have been exchanged.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Apr 16, 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.

@jkczyz jkczyz requested a review from wpaulino April 16, 2025 17:11
@jkczyz
Copy link
Contributor Author

jkczyz commented Apr 16, 2025

The logic around determining if the splice_locked messages have been exchanged is still a work in progress. It may need to be re-considered to work with chained 0-conf splices. See lightning/bolts#1160 (comment).

jkczyz added 2 commits April 18, 2025 17:56
When processing confirmed transactions and updates to the best block,
ChannelManager may be instructed to send a channel_ready message when a
channel's funding transaction has confirmed and has met the required
number of confirmations. A similar action is needed for sending
splice_locked once splice transaction has confirmed with required number
of confirmations. Generalize do_chain_event signature to allow for
either scenario.
When processing confirmed transactions, if the funding transaction is
found then information about it in the ChannelContext is updated. In
preparation for splicing, move this data to FundingScope.
@jkczyz jkczyz force-pushed the 2025-04-splice-locked branch from dfbc04e to e4c0566 Compare April 18, 2025 23:15
@jkczyz
Copy link
Contributor Author

jkczyz commented Apr 18, 2025

@wpaulino Ok, this is in better shape for a high-level look. I don't believe it correctly handles unconfirmed splice transactions yet. Also, doesn't yet re-send splice_locked on channel reestablishment, thought that may come in a follow-up.

Copy link

codecov bot commented Apr 18, 2025

Codecov Report

Attention: Patch coverage is 86.16505% with 57 lines in your changes missing coverage. Please review.

Project coverage is 90.34%. Comparing base (7b45811) to head (ef048e6).
Report is 82 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/ln/channel.rs 90.24% 27 Missing and 5 partials ⚠️
lightning/src/events/mod.rs 0.00% 23 Missing ⚠️
lightning/src/ln/channelmanager.rs 96.15% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3741      +/-   ##
==========================================
+ Coverage   89.10%   90.34%   +1.24%     
==========================================
  Files         156      158       +2     
  Lines      123431   135603   +12172     
  Branches   123431   135603   +12172     
==========================================
+ Hits       109985   122515   +12530     
+ Misses      10760    10568     -192     
+ Partials     2686     2520     -166     

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

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

Copy link
Contributor

@optout21 optout21 left a comment

Choose a reason for hiding this comment

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

LGTM. The preparational changes are very clear. The WIP part also makes sense so far.

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


match pending_splice.sent_funding_txid {
Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => {
debug_assert!(false);
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 need to replay splice_locked if a disconnect happened before they were able to process it. Unclear if we'd want to use this same code path for it or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By this do you mean for channel_reestablish?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I assume there is a similar retransmission case we need to handle there, like with channel_ready.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Gonna leave this for a follow-up, if that's ok.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, probably better to wait for #3637 to land anyway.

@jkczyz jkczyz force-pushed the 2025-04-splice-locked branch from e4c0566 to 49f8ef6 Compare April 29, 2025 19:55
Copy link
Contributor Author

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

Addressed most comments other than adding an event and for re-sending splice_locked. See comment replies for open questions.

Also, added code to insert the new scid in short_to_chan_info. Should we remove the old one?


match pending_splice.sent_funding_txid {
Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => {
debug_assert!(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By this do you mean for channel_reestablish?

@jkczyz jkczyz marked this pull request as ready for review April 30, 2025 16:20
@jkczyz jkczyz added the weekly goal Someone wants to land this this week label Apr 30, 2025
@jkczyz jkczyz force-pushed the 2025-04-splice-locked branch from ef048e6 to f3b0989 Compare April 30, 2025 17:31
@jkczyz
Copy link
Contributor Author

jkczyz commented Apr 30, 2025

Squashed fixups

@wpaulino
Copy link
Contributor

An offline discussion we had around DiscardFunding: we still need to emit one for splices if we negotiated one but it cannot confirm due to the channel closing via a commitment broadcast of the pre-splice FundingScope or a co-op close. Easiest way to handle this may be in transactions_confirmed where we already check for spending transactions of the channel to consider it closed.

@@ -3026,7 +3026,7 @@ macro_rules! locked_close_channel {
// into the map (which prevents the `PeerState` from being cleaned up) for channels that
// never even got confirmations (which would open us up to DoS attacks).
let update_id = $channel_context.get_latest_monitor_update_id();
if $channel_context.get_funding_tx_confirmation_height().is_some() || $channel_context.minimum_depth() == Some(0) || update_id > 1 {
if $channel_funding.get_funding_tx_confirmation_height().is_some() || $channel_context.minimum_depth() == Some(0) || update_id > 1 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought we agreed to try to keep the concept of funding scopes outside of ChannelManager? I think for this we can move to a is_funding_confirmed_or_0conf call or something?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This macro doesn't have access to the channel, only the context which doesn't have reference to the funding scope. So it would require much more re-work than I'd like to do in this PR.

Instead, I'd rather make a separate PR after all the changes needed for FundingScope are complete. Hopefully in a couple PRs.

Copy link
Contributor Author

@jkczyz jkczyz Apr 30, 2025

Choose a reason for hiding this comment

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

Oh, also forgot to drop these two commits since one reverts the previous one. But the general idea still stands for other occurrences.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tried to avoid this but the problem is that locked_close_channel is called throughout the file including by convert_channel_err, which likewise is called throughout. Additionally, convert_channel_err has a rule that expects a FundedChannel so that it can pass it to get_channel_update_for_broadcast.

Might be a way to refactor this but I think it should wait for a follow-up given it's not going to be straightforward.

@@ -11639,7 +11639,7 @@ where
for chan in peer_state.channel_by_id.values().filter_map(Channel::as_funded) {
let txid_opt = chan.funding.get_funding_txo();
let height_opt = chan.context.get_funding_tx_confirmation_height();
let hash_opt = chan.context.get_funding_tx_confirmed_in();
let hash_opt = chan.funding.get_funding_tx_confirmed_in();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we avoid new references to Channel::funding in ChannelManager? I thought we wanted to remove that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was able to get rid of this one by moving get_funding_tx_confirmed_in to FundedChannel since this is the only use.

@@ -3031,7 +3031,7 @@ macro_rules! locked_close_channel {
$peer_state.closed_channel_monitor_update_ids.insert(chan_id, update_id);
}
let mut short_to_chan_info = $self.short_to_chan_info.write().unwrap();
if let Some(short_id) = $channel_context.get_short_channel_id() {
if let Some(short_id) = $channel_funding.get_short_channel_id() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here, can we avoid this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similarly, we'd need this to take a Channel instead of a ChannelContext, but the problem is that convert_channel_err needs a FundedChannel for one rule as mentioned in another comment. Maybe we eventually just add any methods called from those macros on both Channel and each sub-struct so that it can be used either way?

@jkczyz
Copy link
Contributor Author

jkczyz commented May 1, 2025

An offline discussion we had around DiscardFunding: we still need to emit one for splices if we negotiated one but it cannot confirm due to the channel closing via a commitment broadcast of the pre-splice FundingScope or a co-op close. Easiest way to handle this may be in transactions_confirmed where we already check for spending transactions of the channel to consider it closed.

Separate but related to this, I'm wondering now which FundingScope should be used to populate ShutdownResult when we see the commitment transaction of a pending splice confirm presumably in the same block that the splice funding confirmed.

Currently, we use the pre-splice FundingScope since the flow is:

  • ChannelManager::do_chain_event calls
  • FundedChannel::transactions_confirmed which returns ClosureReason to
  • ChannelManager::do_chain_event which calls
  • ChannelContext::force_shutdown with the ClosureReason and FundedChannel::funding and returns a ShutdownResult

After that locked_close_channel! is called with the ShutdownResult and FundedChannel::funding. Then ChannelManager::get_channel_update_for_broadcast which uses the pre-splice FundingScope. Then finally ChannelManager::finish_close_channel is called with the ShutdownResult.

So in the scenario described above, should we transition to the pending FundingScope that was just confirmed and spent -- before calling ChannelContext::force_shutdown -- so that it is reflected in ShutdownResult? We may need to also generate an Event::SpliceLocked, too.

Then that way for Event::DiscardFunding we simply include info for any pending funding scopes in ShutdownResult in order to produce that event. And in the case of transitioning to a pending FundingScope first, we'd avoid producing Event::DiscardFunding since it would not be desired in that situation.

Related discussion: #3592 (comment)

jkczyz added 4 commits May 1, 2025 14:44
When processing confirmed transactions, if the funding transaction is
found then information about it in the ChannelContext is updated. In
preparation for splicing, move this data to FundingScope.
When processing confirmed transactions, if the funding transaction is
found then information about it in the ChannelContext is updated. In
preparation for splicing, move this data to FundingScope.
When checking if channel_ready should be sent, the funding transaction
must reach minimum_depth confirmations. The same logic is needed for
splicing a channel, so refactor it into a helper method.
@jkczyz jkczyz force-pushed the 2025-04-splice-locked branch from f3b0989 to bd1a788 Compare May 1, 2025 21:51
@jkczyz
Copy link
Contributor Author

jkczyz commented May 1, 2025

An offline discussion we had around DiscardFunding: we still need to emit one for splices if we negotiated one but it cannot confirm due to the channel closing via a commitment broadcast of the pre-splice FundingScope or a co-op close. Easiest way to handle this may be in transactions_confirmed where we already check for spending transactions of the channel to consider it closed.

Separate but related to this, I'm wondering now which FundingScope should be used to populate ShutdownResult when we see the commitment transaction of a pending splice confirm presumably in the same block that the splice funding confirmed.

Discussed offline with @TheBlueMatt and @wpaulino. The two scenarios are closing while funding negotiation takes place and while waiting for the splice to confirm. For the latter, it would be better to a have general approach in ChannelMonitor for determining if an Event::DiscardFunding should be emitted. So no changes necessary in this PR.

For the former, we won't have a ChannelMonitor so we will need to emit the event in ChannelManager whenever the user has contributed inputs. I'd imagine we need to keep track of this in PendingSplice and return it to ChannelManager somehow. Also, since we won't have a funding transaction or a funding txo during negotiation, we'd need a new FundingInfo variant for the contributed inputs to use in Event::DiscardFunding, IIUC. (cc: @optout21 @dunxen)

@dunxen dunxen self-requested a review May 5, 2025 18:26
@jkczyz jkczyz requested a review from wpaulino May 6, 2025 20:54
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.

I think i responded to all the active discussions

}

let funding_tx_confirmations = height as i64 - funding.funding_tx_confirmation_height as i64 + 1;
if funding_tx_confirmations < minimum_depth.unwrap_or(0) as i64 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I do kinda wonder if we want to reuse the channel-open minimum_depth for splice lock-in. For channel opens the user can always set the minimum_depth by overriding the config in the inbound manual-accept pipeline or by overriding the config in the outbound open init. I'm not entirely sure a user will want to override the minimum_depth for a splice, but if eg they overrode it for a channel open to set it lower cause the channel was low value suddenly a splice would increase their risk which they didn't intend. Maybe we can move the min-depth setting into the live channel config so users can update it? Either way docs for the min-depth setting should be updated to note it captures splices.

jkczyz added 10 commits May 14, 2025 14:49
The minimum_depth of a channel is overridden to COINBASE_MATURITY if the
funding transaction is the coinbase transaction. However, if this is to
be reused for a splice's minimum_depth, it would be a problem since
sending splice_locked would be unnecessarily delayed. Now that
FundingScope contains the funding transaction, use this to check if it
is a coinbase transaction instead of overriding minimum_depth.
FundingScope::funding_tx_confirmation_height is reset as part of calling
ChannelContext::check_funding_meets_minimum_depth via
FundedChannel::check_get_channel_ready. This side effect requires using
mutable references to self when otherwise it would not be needed.
Instead of reseting funding_tx_confirmation_height there, do so when
unconfirming the funding transaction.
0-conf channels always meet the funding minimum depth once accepted.
Special case this in check_funding_meets_minimum_depth such that it
isn't implicit in later calculations. Since a minimum depth is always
set when the channel is accepted, expect this to be the case in the
method since it should only be called on a ChannelContext in a
FundedChannel.
When transactions confirm or the best block is updated, check if any
pending splice funding transactions have confirmed to an acceptable
depth. If so, send a splice_locked message to the counterparty and -- if
the counterparty has exchanged a splice_locked message for the same
funding txid -- promote the corresponding FundingScope such that the new
funding can be utilized.
@jkczyz jkczyz force-pushed the 2025-04-splice-locked branch from bd6ceea to d72d8b5 Compare May 14, 2025 22:12
Copy link
Contributor Author

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

More comments addressed along with some still unresolved needing feedback. Getting closer...

// time we saw and it will be ignored.
let best_time = self.context.update_time_counter;

funding.funding_tx_confirmation_height = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doing so causes some reorg tests to fail. I can avoid some failures by moving the self.funding.funding_tx_confirmed_in reset after the call to do_best_block_updated as it is used in there. But doing the same for self.funding.short_channel_id causes other failures. Specifically, short_to_chan_info is not cleared.

.map(|tx| tx.is_coinbase())
.unwrap_or(false);

let minimum_depth = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@wpaulino Requested it in #3741 (comment). An earlier version moved minimum_depth to FundingScope because otherwise we would use COINBASE_MATURITY for splices of channels established with a coinbase transaction. But this resulted in needing to still keep around the original minimum_depth in ChannelContext. The workaround was to keep minimum_depth in ChannelContext and have a special case here for the coinbase transaction.

};

match pending_splice.sent_funding_txid {
Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => {
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... we could do that but FundingScope is also used for establishment and hangs around once the splice is locked. So the additional field isn't meaningful then. Maybe it doesn't matter?

Just hate to have a field that isn't needed (or only needed) after a certain point. That's kinda the situation with ChannelContext where we have Option fields that should always be Some.

We'd also need to unset the bool in every other FundingScope if the counterparty sends splice_locked again with a different funding_txid. Seem kinda fragile as we need to maintain an invariant that the bool is true for at most one FundingScope in the list.

@@ -4851,7 +4851,24 @@ impl<SP: Deref> ChannelContext<SP> where SP::Target: SignerProvider {
}

fn check_funding_confirmations(&self, funding: &mut FundingScope, height: u32) -> bool {
if funding.funding_tx_confirmation_height == 0 && self.minimum_depth != Some(0) {
let is_coinbase = funding
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'd need to pass in the appropriate minimum_depth at each call site, which is kinda meh.

return Ok(self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block.height, logger));
}

// TODO: Close channel?
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... docs indicate that that is for when the user calls ChannelManager::force_close_channel.

let persist = match &res {
Err(e) if e.closes_channel() => NotifyOption::DoPersist,
Err(_) => NotifyOption::SkipPersistHandleEvents,
Ok(()) => NotifyOption::SkipPersistHandleEvents,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated to persist when returning OK as discussed offline.

Comment on lines 4880 to 4883
let funding_tx_confirmations = height as i64 - funding.funding_tx_confirmation_height as i64 + 1;
if funding_tx_confirmations <= 0 {
funding.funding_tx_confirmation_height = 0;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to check_funding_meets_minimum_depth and made a separate commit to special case 0-conf.

@jkczyz jkczyz requested review from TheBlueMatt and wpaulino May 14, 2025 22:15
jkczyz added 13 commits May 15, 2025 14:31
This method is only applicable for FundedChannel, so it shouldn't be
accessible from ChannelContext.
Now that ChannelContext::minimum_depth is not overridden with
COINBASE_MATURITY when the funding transaction is the coinbase
transaction, so do instead in the identically named method. Also, add a
minimum_depth to Channel. The one on ChannelContext can become private
once FudningScope doesn't need to be accessed directly from a
ChannelManager macro.

This fixes ChannelDetails showing an incorrect minimum depth when the
coinbase transaction is used to funded the channel.
When a splice funding transaction is unconfirmed, update the
corresponding FundingScope just as is done when the initial funding
transaction is unconfirmed.
Pending funding transactions for splices should be monitored for
appearance on chain. Include these in ChannelManager::get_relevant_txids
so that they can be watched.
Once both parties have exchanged splice_locked messages, the splice
funding is ready for use. Emit an event to the user indicating as much.
A ChannelReady event is used for both channel establishment and splicing
to indicate that the funding transaction is confirmed to an acceptable
depth and thus the channel can be used with the funding. An upcoming
SplicePending event will be emitted for each pending splice (i.e., both
the initial splice attempt and any RBF attempts). Thus, when a
ChannelReady event is emitted, the funding_txo must be included to
differentiate between which ChannelPending -- which also contains the
funding_txo -- that the event corresponds to.
@jkczyz jkczyz force-pushed the 2025-04-splice-locked branch from d72d8b5 to 66fa294 Compare May 15, 2025 22:30
Copy link
Contributor Author

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

}
}

self.context.check_for_funding_tx_spent(&self.funding, tx, logger)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Resolved in an earlier fixup (9b2b24b).

let mut confirmed_funding = None;
#[cfg(splicing)]
for funding in self.pending_funding.iter_mut() {
if confirmed_funding.is_some() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 8efe7c9.

},
};

Err(MsgHandleErrInternal::send_err_msg_no_close("TODO(splicing): Splicing is not implemented (splice_locked)".to_owned(), msg.channel_id))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess we may as well return Ok given we are enqueuing messages and events? This was copied from the other handlers.

.map(|tx| tx.is_coinbase())
.unwrap_or(false);

let minimum_depth = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added a commit to make minimum_depth accurate in ChannelDetails.


#[cfg(splicing)]
fn check_get_splice_locked(
&mut self, pending_splice: &PendingSplice, funding: &mut FundingScope, height: u32,
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 figured as much. Will wait until all other comments are addressed before seeing to this. Not quite sure the added complexity would be worth it.

Went ahead with tracking the index instead of holding a mutable reference and moved the appropriate methods to FundedChannel.

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
None yet
Development

Successfully merging this pull request may close these issues.

5 participants