Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2f8125b
Add NegotiationFailureReason to SpliceFailed event
jkczyz Mar 25, 2026
1035615
f - Fix broken rustdoc links in NegotiationFailureReason
jkczyz Apr 3, 2026
f320e58
f - Add TODO about NegotiationFailureReason layering in propose_quies…
jkczyz Apr 3, 2026
47bd416
f - Use upgradable serialization for NegotiationFailureReason
jkczyz Apr 3, 2026
3cd1d05
Add FundingContribution to SpliceFailed event
jkczyz Mar 25, 2026
b51a7d5
f - Fix FundingTemplate rustdoc link path
jkczyz Apr 3, 2026
36c511b
f - Destructure SpliceFundingFailed to avoid unnecessary clone
jkczyz Apr 3, 2026
f707945
f - Add SpliceFundingFailed::into_parts to avoid empty DiscardFunding…
jkczyz Apr 3, 2026
2fd414e
f - Remove dead funding_txo and channel_type from SpliceFundingFailed
jkczyz Mar 27, 2026
e834852
Emit DiscardFunding before SpliceFailed
jkczyz Mar 25, 2026
df6b58b
Rename SplicePending and SpliceFailed events
jkczyz Mar 25, 2026
e34b3ac
f - Update changelog to use SpliceNegotiationFailed name
jkczyz Apr 3, 2026
4a57ac8
Simplify contribution pop in reset_pending_splice_state
jkczyz Mar 27, 2026
8946f49
Fix output filtering in into_unique_contributions
jkczyz Mar 27, 2026
bfabcb5
Derive SpliceFundingFailed inputs from FundingContribution
jkczyz Mar 27, 2026
3ee03ae
f - Add debug_asserts for funding_negotiation invariant
jkczyz Apr 3, 2026
8b0148a
f - Remove dead contributed_inputs_and_outputs methods from FundingNe…
jkczyz Apr 3, 2026
d35271c
Remove unused NegotiationError and contributed_inputs_and_outputs met…
jkczyz Mar 27, 2026
44ecc06
Add pending changelog for splice negotiation event changes
jkczyz Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions fuzz/src/chanmon_consistency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2015,7 +2015,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
)
.unwrap();
},
events::Event::SplicePending { new_funding_txo, .. } => {
events::Event::SpliceNegotiated { new_funding_txo, .. } => {
let broadcaster = match $node {
0 => &broadcast_a,
1 => &broadcast_b,
Expand All @@ -2027,7 +2027,7 @@ pub fn do_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) {
assert_eq!(new_funding_txo.txid, splice_tx.compute_txid());
chain_state.confirm_tx(splice_tx);
},
events::Event::SpliceFailed { .. } => {},
events::Event::SpliceNegotiationFailed { .. } => {},
events::Event::DiscardFunding {
funding_info: events::FundingInfo::Contribution { .. },
..
Expand Down
4 changes: 2 additions & 2 deletions fuzz/src/full_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,10 +1141,10 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>
signed_tx,
);
},
Event::SplicePending { .. } => {
Event::SpliceNegotiated { .. } => {
// Splice negotiation completed, waiting for confirmation
},
Event::SpliceFailed { .. } => {
Event::SpliceNegotiationFailed { .. } => {
// Splice failed, inputs can be re-spent
},
Event::OpenChannelRequest {
Expand Down
118 changes: 93 additions & 25 deletions lightning/src/events/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +100,65 @@ 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.
#[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. Retry by calling
/// [`ChannelManager::splice_channel`] after the peer reconnects.
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
PeerDisconnected,
/// The counterparty aborted the negotiation by sending `tx_abort`. Retry by calling
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Almost each docstring says "Retry by calling splice_channel". That doesn't seem like particularly helpful advice especially in cases where the counterparty rejected implying just trying again is unlikely to fix it?

/// [`ChannelManager::splice_channel`], or wait for the counterparty to initiate.
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
CounterpartyAborted,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is the "Message data" in tx_abort not a string that we can include here?

/// An error occurred while negotiating the interactive transaction (e.g., the counterparty
/// sent an invalid message). Retry by calling [`ChannelManager::splice_channel`].
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
NegotiationError,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Note we could have this wrap AbortReason -- we had an internal NegotiationError struct that already wrapped this along with contributions -- but it would require making AbortReason public. Not sure if we want to expose all its variants.

/// The funding contribution was invalid (e.g., insufficient balance for the splice amount).
/// Adjust the contribution and retry via [`ChannelManager::splice_channel`].
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
ContributionInvalid,
Copy link
Copy Markdown
Collaborator

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_channel again" 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.

Copy link
Copy Markdown
Contributor Author

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_channel isn't always necessary? In some cases, calling funding_contributed at a later time may be sufficient. We are providing the FundingContribution after all.

So it does seem like different actions are required depending on the variant.

  • PeerDisconnected: retry upon re-connection
  • ChannelNotReady: retry when channel is in a usable state (assuming it's not closing)
  • FeeRateTooLow: retry with a higher fee rate
  • ContributionInvalid: retry with a re-computed contribution
  • NegotiationError: probably not retryable as it indicates a counterparty error
  • CounterpartyAborted: might be able to retry but would need to examine the tx_abort message (e.g., counterparty doesn't like your fee rate).

We could have a top-level Retryable that holds one of these instead, if you prefer? We could go as far as only putting the FundingContribution in applicable sub-variants. Though a user may want to see it for other variants.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I assume ChannelNotReady is really "you can't retry" (why were you splicing an unopened channel anyway? but probably its closing), FeeRateTooLow and ContributionInvalid are really "retry now with a fresh splice_channel call 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 fresh splice_channel call, or not", with the one maybe-exception of PeerDisconnected? 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?

Copy link
Copy Markdown
Contributor Author

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.

Copy link
Copy Markdown
Contributor Author

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_channel again upon re-connection. Otherwise, we'd need to make sure not to process the associated DiscardFunding which would mean locking up the UTXOs indefinitely. Instead, the client can simply always queue up a "retry when connected".

/// The negotiation was locally abandoned via `ChannelManager::abandon_splice`.
LocallyAbandoned,
/// The channel was not in a state to accept the funding contribution. Retry by calling
/// [`ChannelManager::splice_channel`] once [`ChannelDetails::is_usable`] returns `true`.
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
/// [`ChannelDetails::is_usable`]: crate::ln::channel_state::ChannelDetails::is_usable
ChannelNotReady,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
/// The contribution's feerate was too low. Retry with a higher feerate by calling
/// [`ChannelManager::splice_channel`] to obtain a new [`FundingTemplate`].
///
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
/// [`FundingTemplate`]: crate::ln::funding::FundingTemplate
FeeRateTooLow,
}

impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(0, Unknown) => {},
(2, PeerDisconnected) => {},
(4, CounterpartyAborted) => {},
(6, NegotiationError) => {},
(8, ContributionInvalid) => {},
(10, LocallyAbandoned) => {},
(12, ChannelNotReady) => {},
(14, ChannelClosing) => {},
(16, 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)]
Expand Down Expand Up @@ -1541,8 +1601,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.
Expand All @@ -1560,19 +1620,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.
Expand All @@ -1582,10 +1643,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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Didn't feel like there was much value to these.

  • abandoned_funding_txo would be set only if there's a failure after the transaction was negotiated but before signed.
  • In the future, when channel_type can change, maybe it is just part of FundingContribution? Could also keep it if you prefer.

/// 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.
Expand Down Expand Up @@ -2355,7 +2423,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,
Expand All @@ -2373,20 +2441,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
Expand Down Expand Up @@ -3012,7 +3080,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(),
Expand All @@ -3027,18 +3095,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()
Expand Down
8 changes: 4 additions & 4 deletions lightning/src/ln/async_signer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1647,8 +1647,8 @@ fn test_async_splice_initial_commit_sig() {
get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id);
acceptor.node.handle_tx_signatures(initiator_node_id, &tx_signatures);

let _ = get_event!(initiator, Event::SplicePending);
let _ = get_event!(acceptor, Event::SplicePending);
let _ = get_event!(initiator, Event::SpliceNegotiated);
let _ = get_event!(acceptor, Event::SpliceNegotiated);
}

#[test]
Expand Down Expand Up @@ -1739,6 +1739,6 @@ fn test_async_splice_initial_commit_sig_waits_for_monitor_before_tx_signatures()
get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id);
acceptor.node.handle_tx_signatures(initiator_node_id, &tx_signatures);

let _ = get_event!(initiator, Event::SplicePending);
let _ = get_event!(acceptor, Event::SplicePending);
let _ = get_event!(initiator, Event::SpliceNegotiated);
let _ = get_event!(acceptor, Event::SpliceNegotiated);
}
Loading
Loading