-
Notifications
You must be signed in to change notification settings - Fork 452
Include failure context in splice events #4514
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
jkczyz
wants to merge
9
commits into
lightningdevkit:main
Choose a base branch
from
jkczyz:2026-03-splice-rbf-fail-event
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
e82e4e5
Add NegotiationFailureReason to SpliceFailed event
jkczyz e11cbe0
Add FundingContribution to SpliceFailed event
jkczyz d5ec0af
Emit DiscardFunding before SpliceFailed
jkczyz 3a810de
Rename SplicePending and SpliceFailed events
jkczyz bd6fed9
Simplify contribution pop in reset_pending_splice_state
jkczyz 9bc81a2
Fix output filtering in into_unique_contributions
jkczyz ad63946
Derive SpliceFundingFailed inputs from FundingContribution
jkczyz c113974
Remove unused NegotiationError and contributed_inputs_and_outputs met…
jkczyz 0287ceb
Add pending changelog for splice negotiation event changes
jkczyz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ use crate::blinded_path::payment::{ | |
| use crate::chain::transaction; | ||
| use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; | ||
| use crate::ln::channelmanager::{InterceptId, PaymentId}; | ||
| use crate::ln::funding::FundingContribution; | ||
| use crate::ln::msgs; | ||
| use crate::ln::onion_utils::LocalHTLCFailureReason; | ||
| use crate::ln::outbound_payment::RecipientOnionFields; | ||
|
|
@@ -99,6 +100,109 @@ impl_writeable_tlv_based_enum!(FundingInfo, | |
| } | ||
| ); | ||
|
|
||
| /// The reason a funding negotiation round failed. | ||
| /// | ||
| /// Each negotiation attempt (initial or RBF) resolves to either success or failure. This enum | ||
| /// indicates what caused the failure. Use [`is_retriable`] to determine whether the splice can | ||
| /// be reattempted on this channel by calling [`ChannelManager::splice_channel`]. | ||
| /// | ||
| /// [`is_retriable`]: Self::is_retriable | ||
| /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel | ||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub enum NegotiationFailureReason { | ||
| /// The reason was not available (e.g., from an older serialization). | ||
| Unknown, | ||
| /// The peer disconnected during negotiation. Wait for the peer to reconnect, then retry. | ||
| PeerDisconnected, | ||
| /// The counterparty explicitly aborted the negotiation by sending `tx_abort`. Retrying with | ||
| /// the same parameters is unlikely to succeed — consider adjusting the contribution or | ||
| /// waiting for the counterparty to initiate. | ||
| CounterpartyAborted { | ||
| /// The counterparty's abort message. | ||
| /// | ||
| /// This is counterparty-provided data. Use `Display` on [`UntrustedString`] for safe | ||
| /// logging. | ||
| msg: UntrustedString, | ||
| }, | ||
| /// An error occurred during interactive transaction negotiation (e.g., the counterparty sent | ||
| /// an invalid message). The negotiation was aborted. | ||
| NegotiationError { | ||
| /// A developer-readable error message. | ||
| msg: String, | ||
| }, | ||
| /// The funding contribution was invalid (e.g., insufficient balance for the splice amount). | ||
| /// Call [`ChannelManager::splice_channel`] for a fresh [`FundingTemplate`] and build a new | ||
| /// contribution with adjusted parameters. | ||
| /// | ||
| /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel | ||
| /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate | ||
| ContributionInvalid, | ||
| /// The negotiation was locally abandoned via `ChannelManager::abandon_splice`. | ||
| LocallyAbandoned, | ||
| /// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`] | ||
| /// for the closure reason. | ||
| ChannelClosing, | ||
| /// The contribution's feerate was too low for RBF. Call [`ChannelManager::splice_channel`] | ||
| /// for a fresh [`FundingTemplate`] (which includes the updated minimum feerate) and build a | ||
| /// new contribution with a higher feerate. | ||
| /// | ||
| /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel | ||
| /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate | ||
| FeeRateTooLow, | ||
| } | ||
|
|
||
| impl NegotiationFailureReason { | ||
| /// Whether the splice negotiation is likely to succeed if retried on this channel. When `true`, | ||
| /// call [`ChannelManager::splice_channel`] to obtain a fresh [`FundingTemplate`] and retry. | ||
| /// | ||
| /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel | ||
| /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate | ||
| pub fn is_retriable(&self) -> bool { | ||
| match self { | ||
| Self::Unknown | ||
| | Self::PeerDisconnected | ||
| | Self::CounterpartyAborted { .. } | ||
| | Self::NegotiationError { .. } | ||
| | Self::ContributionInvalid | ||
| | Self::FeeRateTooLow => true, | ||
| Self::LocallyAbandoned | Self::ChannelClosing => false, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl core::fmt::Display for NegotiationFailureReason { | ||
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
| match self { | ||
| Self::Unknown => f.write_str("unknown reason"), | ||
| Self::PeerDisconnected => f.write_str("peer disconnected during negotiation"), | ||
| Self::CounterpartyAborted { msg } => { | ||
| write!(f, "counterparty aborted: {}", msg) | ||
| }, | ||
| Self::NegotiationError { msg } => write!(f, "negotiation error: {}", msg), | ||
| Self::ContributionInvalid => f.write_str("funding contribution was invalid"), | ||
| Self::LocallyAbandoned => f.write_str("splice locally abandoned"), | ||
|
|
||
| Self::ChannelClosing => f.write_str("channel is closing"), | ||
| Self::FeeRateTooLow => f.write_str("feerate too low for RBF"), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason, | ||
| (0, Unknown) => {}, | ||
| (2, PeerDisconnected) => {}, | ||
| (4, CounterpartyAborted) => { | ||
| (1, msg, required), | ||
| }, | ||
| (6, NegotiationError) => { | ||
| (1, msg, required), | ||
| }, | ||
| (8, ContributionInvalid) => {}, | ||
| (10, LocallyAbandoned) => {}, | ||
| (12, ChannelClosing) => {}, | ||
| (14, FeeRateTooLow) => {}, | ||
| ); | ||
|
|
||
| /// Some information provided on receipt of payment depends on whether the payment received is a | ||
| /// spontaneous payment or a "conventional" lightning payment that's paying an invoice. | ||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
|
|
@@ -1541,8 +1645,8 @@ pub enum Event { | |
| /// # Failure Behavior and Persistence | ||
| /// This event will eventually be replayed after failures-to-handle (i.e., the event handler | ||
| /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. | ||
| SplicePending { | ||
| /// The `channel_id` of the channel that has a pending splice funding transaction. | ||
| SpliceNegotiated { | ||
| /// The `channel_id` of the channel with the negotiated splice funding transaction. | ||
| channel_id: ChannelId, | ||
| /// The `user_channel_id` value passed in to [`ChannelManager::create_channel`] for outbound | ||
| /// channels, or to [`ChannelManager::accept_inbound_channel`] for inbound channels. | ||
|
|
@@ -1560,19 +1664,20 @@ pub enum Event { | |
| /// The witness script that is used to lock the channel's funding output to commitment transactions. | ||
| new_funding_redeem_script: ScriptBuf, | ||
| }, | ||
| /// Used to indicate that a splice for the given `channel_id` has failed. | ||
| /// Used to indicate that a splice negotiation round for the given `channel_id` has failed. | ||
| /// | ||
| /// This event may be emitted if a splice fails after it has been initiated but prior to signing | ||
| /// any negotiated funding transaction. | ||
| /// Each splice attempt (initial or RBF) resolves to either [`Event::SpliceNegotiated`] on | ||
| /// success or this event on failure. Prior successfully negotiated splice transactions are | ||
| /// unaffected. | ||
| /// | ||
| /// Any UTXOs contributed to be spent by the funding transaction may be reused and will be | ||
| /// given in `contributed_inputs`. | ||
| /// Any UTXOs contributed to the failed round that are not committed to a prior negotiated | ||
| /// splice transaction will be returned via a preceding [`Event::DiscardFunding`]. | ||
| /// | ||
| /// # Failure Behavior and Persistence | ||
| /// This event will eventually be replayed after failures-to-handle (i.e., the event handler | ||
| /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. | ||
| SpliceFailed { | ||
| /// The `channel_id` of the channel for which the splice failed. | ||
| SpliceNegotiationFailed { | ||
| /// The `channel_id` of the channel for which the splice negotiation round failed. | ||
| channel_id: ChannelId, | ||
| /// The `user_channel_id` value passed in to [`ChannelManager::create_channel`] for outbound | ||
| /// channels, or to [`ChannelManager::accept_inbound_channel`] for inbound channels. | ||
|
|
@@ -1582,10 +1687,17 @@ pub enum Event { | |
| user_channel_id: u128, | ||
| /// The `node_id` of the channel counterparty. | ||
| counterparty_node_id: PublicKey, | ||
| /// The outpoint of the channel's splice funding transaction, if one was created. | ||
| abandoned_funding_txo: Option<OutPoint>, | ||
| /// The features that this channel will operate with, if available. | ||
| channel_type: Option<ChannelTypeFeatures>, | ||
|
Comment on lines
-1585
to
-1588
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Didn't feel like there was much value to these.
|
||
| /// The reason the splice negotiation failed. | ||
| reason: NegotiationFailureReason, | ||
| /// The funding contribution from the failed negotiation round, if available. This can be | ||
| /// fed back to [`ChannelManager::funding_contributed`] to retry with the same parameters. | ||
| /// Alternatively, call [`ChannelManager::splice_channel`] to obtain a fresh | ||
| /// [`FundingTemplate`] and build a new contribution. | ||
| /// | ||
| /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed | ||
| /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel | ||
| /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate | ||
| contribution: Option<FundingContribution>, | ||
| }, | ||
| /// Used to indicate to the user that they can abandon the funding transaction and recycle the | ||
| /// inputs for another purpose. | ||
|
|
@@ -2355,7 +2467,7 @@ impl Writeable for Event { | |
| // We never write out FundingTransactionReadyForSigning events as they will be regenerated when | ||
| // necessary. | ||
| }, | ||
| &Event::SplicePending { | ||
| &Event::SpliceNegotiated { | ||
| ref channel_id, | ||
| ref user_channel_id, | ||
| ref counterparty_node_id, | ||
|
|
@@ -2373,20 +2485,20 @@ impl Writeable for Event { | |
| (11, new_funding_redeem_script, required), | ||
| }); | ||
| }, | ||
| &Event::SpliceFailed { | ||
| &Event::SpliceNegotiationFailed { | ||
| ref channel_id, | ||
| ref user_channel_id, | ||
| ref counterparty_node_id, | ||
| ref abandoned_funding_txo, | ||
| ref channel_type, | ||
| ref reason, | ||
| ref contribution, | ||
| } => { | ||
| 52u8.write(writer)?; | ||
| write_tlv_fields!(writer, { | ||
| (1, channel_id, required), | ||
| (3, channel_type, option), | ||
| (5, user_channel_id, required), | ||
| (7, counterparty_node_id, required), | ||
| (9, abandoned_funding_txo, option), | ||
| (11, reason, required), | ||
| (13, contribution, option), | ||
| }); | ||
| }, | ||
| // Note that, going forward, all new events must only write data inside of | ||
|
|
@@ -3012,7 +3124,7 @@ impl MaybeReadable for Event { | |
| (11, new_funding_redeem_script, required), | ||
| }); | ||
|
|
||
| Ok(Some(Event::SplicePending { | ||
| Ok(Some(Event::SpliceNegotiated { | ||
| channel_id: channel_id.0.unwrap(), | ||
| user_channel_id: user_channel_id.0.unwrap(), | ||
| counterparty_node_id: counterparty_node_id.0.unwrap(), | ||
|
|
@@ -3027,18 +3139,18 @@ impl MaybeReadable for Event { | |
| let mut f = || { | ||
| _init_and_read_len_prefixed_tlv_fields!(reader, { | ||
| (1, channel_id, required), | ||
| (3, channel_type, option), | ||
| (5, user_channel_id, required), | ||
| (7, counterparty_node_id, required), | ||
| (9, abandoned_funding_txo, option), | ||
| (11, reason, upgradable_option), | ||
| (13, contribution, option), | ||
| }); | ||
|
|
||
| Ok(Some(Event::SpliceFailed { | ||
| Ok(Some(Event::SpliceNegotiationFailed { | ||
| channel_id: channel_id.0.unwrap(), | ||
| user_channel_id: user_channel_id.0.unwrap(), | ||
| counterparty_node_id: counterparty_node_id.0.unwrap(), | ||
| abandoned_funding_txo, | ||
| channel_type, | ||
| reason: reason.unwrap_or(NegotiationFailureReason::Unknown), | ||
| contribution, | ||
| })) | ||
| }; | ||
| f() | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does seem like some of these could use more details (error message string?). As-is given the recommendation for ~all enum variants is "retry by calling
splice_channelagain" its not really clear to me why we should bother with an enum - an enum is useful if the downstream code has different handling for different cases, but here that doesn't really seem likely/possible. If we can't expose more details that are useful for automated action, we could probably do with an enum with three variants and a developer-readable string.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the docs could be improved here. Maybe re-calling
splice_channelisn't always necessary? In some cases, callingfunding_contributedat a later time may be sufficient. We are providing theFundingContributionafter all.So it does seem like different actions are required depending on the variant.
PeerDisconnected: retry upon re-connectionChannelNotReady: retry when channel is in a usable state (assuming it's not closing)FeeRateTooLow: retry with a higher fee rateContributionInvalid: retry with a re-computed contributionNegotiationError: probably not retryable as it indicates a counterparty errorCounterpartyAborted: might be able to retry but would need to examine thetx_abortmessage (e.g., counterparty doesn't like your fee rate).We could have a top-level
Retryablethat holds one of these instead, if you prefer? We could go as far as only putting theFundingContributionin applicable sub-variants. Though a user may want to see it for other variants.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume
ChannelNotReadyis really "you can't retry" (why were you splicing an unopened channel anyway? but probably its closing),FeeRateTooLowandContributionInvalidare really "retry now with a freshsplice_channelcall as that'll tell you what the new required feerate is to do a fresh rbf from scracth or will fix your contribution, hopefully". So it does seem like its basically "retry with a freshsplice_channelcall, or not", with the one maybe-exception ofPeerDisconnected? We could leave the enum and include a function that returns "retryable or not", but I'm not really sure why we shouldn't just return a string+bool pair (in a struct) - do we anticipate downstream code will have any different behavior based on anything beyond "should I retry this" or is it just gonna log/print an error?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not other than when / how should you retry in case of the
PeerDisconnected.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose given we don't know when the peer will reconnect, we might as well just process it the same and call
splice_channelagain upon re-connection. Otherwise, we'd need to make sure not to process the associatedDiscardFundingwhich would mean locking up the UTXOs indefinitely. Instead, the client can simply always queue up a "retry when connected".There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Discussed offline. Updated to have an
is_retryablemethod that the counterparty calls rather than examining the variant. Would prevent allocating strings in many cases.