Skip to content

Commit 2f8125b

Browse files
jkczyzclaude
andcommitted
Add NegotiationFailureReason to SpliceFailed event
Each splice negotiation round can fail for different reasons, but Event::SpliceFailed previously gave no indication of what went wrong. Add a NegotiationFailureReason enum so users can distinguish failures and take appropriate action (e.g., retry with a higher feerate vs. wait for the channel to become usable). The reason is determined at each channelmanager emission site based on context rather than threaded through channel.rs internals, since the channelmanager knows the triggering context (disconnect, tx_abort, shutdown, etc.) while channel.rs functions like abandon_quiescent_action handle both splice and non-splice quiescent actions. The one exception is QuiescentError::FailSplice, which carries a reason alongside the SpliceFundingFailed. This is appropriate because FailSplice is already splice-specific, and the channel.rs code that constructs it (e.g., contribution validation, feerate checks) knows the specific failure cause. A with_negotiation_failure_reason method on QuiescentError allows callers to override the default when needed. Older serializations that lack the reason field default to Unknown via default_value in deserialization. The persistence reload path uses PeerDisconnected since a reload implies the peer connection was lost. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2adb690 commit 2f8125b

5 files changed

Lines changed: 227 additions & 41 deletions

File tree

lightning/src/events/mod.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,67 @@ impl_writeable_tlv_based_enum!(FundingInfo,
9999
}
100100
);
101101

102+
/// The reason a funding negotiation round failed.
103+
///
104+
/// Each negotiation attempt (initial or RBF) resolves to either success or failure. This enum
105+
/// indicates what caused the failure.
106+
#[derive(Clone, Debug, PartialEq, Eq)]
107+
pub enum NegotiationFailureReason {
108+
/// The reason was not available (e.g., from an older serialization).
109+
Unknown,
110+
/// The peer disconnected during negotiation. Retry by calling
111+
/// [`ChannelManager::splice_channel`] after the peer reconnects.
112+
///
113+
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
114+
PeerDisconnected,
115+
/// The counterparty aborted the negotiation by sending `tx_abort`. Retry by calling
116+
/// [`ChannelManager::splice_channel`], or wait for the counterparty to initiate.
117+
///
118+
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
119+
CounterpartyAborted,
120+
/// An error occurred while negotiating the interactive transaction (e.g., the counterparty
121+
/// sent an invalid message). Retry by calling [`ChannelManager::splice_channel`].
122+
///
123+
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
124+
NegotiationError,
125+
/// The funding contribution was invalid (e.g., insufficient balance for the splice amount).
126+
/// Adjust the contribution and retry via [`ChannelManager::splice_channel`].
127+
///
128+
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
129+
ContributionInvalid,
130+
/// The negotiation was locally abandoned via [`ChannelManager::abandon_splice`].
131+
///
132+
/// [`ChannelManager::abandon_splice`]: crate::ln::channelmanager::ChannelManager::abandon_splice
133+
LocallyAbandoned,
134+
/// The channel was not in a state to accept the funding contribution. Retry by calling
135+
/// [`ChannelManager::splice_channel`] once [`ChannelDetails::is_usable`] returns `true`.
136+
///
137+
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
138+
/// [`ChannelDetails::is_usable`]: crate::ln::channelmanager::ChannelDetails::is_usable
139+
ChannelNotReady,
140+
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
141+
/// for the closure reason.
142+
ChannelClosing,
143+
/// The contribution's feerate was too low. Retry with a higher feerate by calling
144+
/// [`ChannelManager::splice_channel`] to obtain a new [`FundingTemplate`].
145+
///
146+
/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel
147+
/// [`FundingTemplate`]: crate::ln::channelmanager::FundingTemplate
148+
FeeRateTooLow,
149+
}
150+
151+
impl_writeable_tlv_based_enum!(NegotiationFailureReason,
152+
(0, Unknown) => {},
153+
(2, PeerDisconnected) => {},
154+
(4, CounterpartyAborted) => {},
155+
(6, NegotiationError) => {},
156+
(8, ContributionInvalid) => {},
157+
(10, LocallyAbandoned) => {},
158+
(12, ChannelNotReady) => {},
159+
(14, ChannelClosing) => {},
160+
(16, FeeRateTooLow) => {},
161+
);
162+
102163
/// Some information provided on receipt of payment depends on whether the payment received is a
103164
/// spontaneous payment or a "conventional" lightning payment that's paying an invoice.
104165
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -1586,6 +1647,8 @@ pub enum Event {
15861647
abandoned_funding_txo: Option<OutPoint>,
15871648
/// The features that this channel will operate with, if available.
15881649
channel_type: Option<ChannelTypeFeatures>,
1650+
/// The reason the splice negotiation failed.
1651+
reason: NegotiationFailureReason,
15891652
},
15901653
/// Used to indicate to the user that they can abandon the funding transaction and recycle the
15911654
/// inputs for another purpose.
@@ -2379,6 +2442,7 @@ impl Writeable for Event {
23792442
ref counterparty_node_id,
23802443
ref abandoned_funding_txo,
23812444
ref channel_type,
2445+
ref reason,
23822446
} => {
23832447
52u8.write(writer)?;
23842448
write_tlv_fields!(writer, {
@@ -2387,6 +2451,7 @@ impl Writeable for Event {
23872451
(5, user_channel_id, required),
23882452
(7, counterparty_node_id, required),
23892453
(9, abandoned_funding_txo, option),
2454+
(11, reason, required),
23902455
});
23912456
},
23922457
// Note that, going forward, all new events must only write data inside of
@@ -3031,6 +3096,7 @@ impl MaybeReadable for Event {
30313096
(5, user_channel_id, required),
30323097
(7, counterparty_node_id, required),
30333098
(9, abandoned_funding_txo, option),
3099+
(11, reason, (default_value, NegotiationFailureReason::Unknown)),
30343100
});
30353101

30363102
Ok(Some(Event::SpliceFailed {
@@ -3039,6 +3105,7 @@ impl MaybeReadable for Event {
30393105
counterparty_node_id: counterparty_node_id.0.unwrap(),
30403106
abandoned_funding_txo,
30413107
channel_type,
3108+
reason: reason.0.unwrap(),
30423109
}))
30433110
};
30443111
f()

lightning/src/ln/channel.rs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use crate::chain::channelmonitor::{
3636
};
3737
use crate::chain::transaction::{OutPoint, TransactionData};
3838
use crate::chain::BestBlock;
39-
use crate::events::{ClosureReason, FundingInfo};
39+
use crate::events::{ClosureReason, FundingInfo, NegotiationFailureReason};
4040
use crate::ln::chan_utils;
4141
use crate::ln::chan_utils::{
4242
get_commitment_transaction_number_obscure_factor, max_htlcs, second_stage_tx_fees_sat,
@@ -3174,7 +3174,17 @@ pub(crate) enum QuiescentAction {
31743174
pub(super) enum QuiescentError {
31753175
DoNothing,
31763176
DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<bitcoin::TxOut> },
3177-
FailSplice(SpliceFundingFailed),
3177+
FailSplice(SpliceFundingFailed, NegotiationFailureReason),
3178+
}
3179+
3180+
impl QuiescentError {
3181+
fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
3182+
match self {
3183+
QuiescentError::FailSplice(_, ref mut r) => *r = reason,
3184+
_ => debug_assert!(false, "Expected FailSplice variant"),
3185+
}
3186+
self
3187+
}
31783188
}
31793189

31803190
pub(crate) enum StfuResponse {
@@ -7133,9 +7143,10 @@ where
71337143

71347144
fn quiescent_action_into_error(&self, action: QuiescentAction) -> QuiescentError {
71357145
match action {
7136-
QuiescentAction::Splice { contribution, .. } => {
7137-
QuiescentError::FailSplice(self.splice_funding_failed_for(contribution))
7138-
},
7146+
QuiescentAction::Splice { contribution, .. } => QuiescentError::FailSplice(
7147+
self.splice_funding_failed_for(contribution),
7148+
NegotiationFailureReason::Unknown,
7149+
),
71397150
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
71407151
QuiescentAction::DoNothing => QuiescentError::DoNothing,
71417152
}
@@ -7144,7 +7155,7 @@ where
71447155
fn abandon_quiescent_action(&mut self) -> Option<SpliceFundingFailed> {
71457156
let action = self.quiescent_action.take()?;
71467157
match self.quiescent_action_into_error(action) {
7147-
QuiescentError::FailSplice(failed) => Some(failed),
7158+
QuiescentError::FailSplice(failed, _) => Some(failed),
71487159
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
71497160
QuiescentError::DoNothing => None,
71507161
_ => {
@@ -12540,7 +12551,10 @@ where
1254012551
}) {
1254112552
log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e);
1254212553

12543-
return Err(QuiescentError::FailSplice(self.splice_funding_failed_for(contribution)));
12554+
return Err(QuiescentError::FailSplice(
12555+
self.splice_funding_failed_for(contribution),
12556+
NegotiationFailureReason::ContributionInvalid,
12557+
));
1254412558
}
1254512559

1254612560
if let Some(pending_splice) = self.pending_splice.as_ref() {
@@ -12556,6 +12570,7 @@ where
1255612570
);
1255712571
return Err(QuiescentError::FailSplice(
1255812572
self.splice_funding_failed_for(contribution),
12573+
NegotiationFailureReason::FeeRateTooLow,
1255912574
));
1256012575
}
1256112576
}
@@ -13998,7 +14013,8 @@ where
1399814013

1399914014
if !self.context.is_usable() {
1400014015
log_debug!(logger, "Channel is not in a usable state to propose quiescence");
14001-
return Err(self.quiescent_action_into_error(action));
14016+
return Err(self.quiescent_action_into_error(action)
14017+
.with_negotiation_failure_reason(NegotiationFailureReason::ChannelNotReady));
1400214018
}
1400314019
if self.quiescent_action.is_some() {
1400414020
log_debug!(
@@ -14115,7 +14131,10 @@ where
1411514131
self.context.channel_id(),
1411614132
e,
1411714133
)),
14118-
QuiescentError::FailSplice(failed),
14134+
QuiescentError::FailSplice(
14135+
failed,
14136+
NegotiationFailureReason::ContributionInvalid,
14137+
),
1411914138
));
1412014139
}
1412114140
let prior_contribution = contribution.clone();

lightning/src/ln/channelmanager.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4115,6 +4115,7 @@ impl<
41154115
user_channel_id: chan.context().get_user_id(),
41164116
abandoned_funding_txo: splice_funding_failed.funding_txo,
41174117
channel_type: splice_funding_failed.channel_type,
4118+
reason: events::NegotiationFailureReason::ChannelClosing,
41184119
},
41194120
None,
41204121
));
@@ -4421,6 +4422,7 @@ impl<
44214422
user_channel_id: shutdown_res.user_channel_id,
44224423
abandoned_funding_txo: splice_funding_failed.funding_txo,
44234424
channel_type: splice_funding_failed.channel_type,
4425+
reason: events::NegotiationFailureReason::ChannelClosing,
44244426
},
44254427
None,
44264428
));
@@ -4927,6 +4929,7 @@ impl<
49274929
user_channel_id: chan.context.get_user_id(),
49284930
abandoned_funding_txo: splice_funding_failed.funding_txo,
49294931
channel_type: splice_funding_failed.channel_type,
4932+
reason: events::NegotiationFailureReason::LocallyAbandoned,
49304933
},
49314934
None,
49324935
));
@@ -6609,12 +6612,15 @@ impl<
66096612
));
66106613
}
66116614
},
6612-
QuiescentError::FailSplice(SpliceFundingFailed {
6613-
funding_txo,
6614-
channel_type,
6615-
contributed_inputs,
6616-
contributed_outputs,
6617-
}) => {
6615+
QuiescentError::FailSplice(
6616+
SpliceFundingFailed {
6617+
funding_txo,
6618+
channel_type,
6619+
contributed_inputs,
6620+
contributed_outputs,
6621+
},
6622+
reason,
6623+
) => {
66186624
let pending_events = &mut self.pending_events.lock().unwrap();
66196625
pending_events.push_back((
66206626
events::Event::SpliceFailed {
@@ -6623,6 +6629,7 @@ impl<
66236629
user_channel_id,
66246630
abandoned_funding_txo: funding_txo,
66256631
channel_type,
6632+
reason,
66266633
},
66276634
None,
66286635
));
@@ -6764,7 +6771,7 @@ impl<
67646771
"Channel {} already has a pending funding contribution",
67656772
channel_id,
67666773
),
6767-
QuiescentError::FailSplice(_) => format!(
6774+
QuiescentError::FailSplice(..) => format!(
67686775
"Channel {} cannot accept funding contribution",
67696776
channel_id,
67706777
),
@@ -11858,6 +11865,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1185811865
user_channel_id: channel.context().get_user_id(),
1185911866
abandoned_funding_txo: splice_funding_failed.funding_txo,
1186011867
channel_type: splice_funding_failed.channel_type.clone(),
11868+
reason: events::NegotiationFailureReason::NegotiationError,
1186111869
},
1186211870
None,
1186311871
));
@@ -12017,6 +12025,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1201712025
user_channel_id: chan.context().get_user_id(),
1201812026
abandoned_funding_txo: splice_funding_failed.funding_txo,
1201912027
channel_type: splice_funding_failed.channel_type.clone(),
12028+
reason: events::NegotiationFailureReason::NegotiationError,
1202012029
},
1202112030
None,
1202212031
));
@@ -12187,6 +12196,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1218712196
user_channel_id: chan_entry.get().context().get_user_id(),
1218812197
abandoned_funding_txo: splice_funding_failed.funding_txo,
1218912198
channel_type: splice_funding_failed.channel_type,
12199+
reason: events::NegotiationFailureReason::CounterpartyAborted,
1219012200
},
1219112201
None,
1219212202
));
@@ -12335,6 +12345,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1233512345
user_channel_id: chan.context().get_user_id(),
1233612346
abandoned_funding_txo: splice_funding_failed.funding_txo,
1233712347
channel_type: splice_funding_failed.channel_type,
12348+
reason: events::NegotiationFailureReason::ChannelClosing,
1233812349
},
1233912350
None,
1234012351
));
@@ -15451,6 +15462,7 @@ impl<
1545115462
user_channel_id: chan.context().get_user_id(),
1545215463
abandoned_funding_txo: splice_funding_failed.funding_txo,
1545315464
channel_type: splice_funding_failed.channel_type,
15465+
reason: events::NegotiationFailureReason::PeerDisconnected,
1545415466
});
1545515467
splice_failed_events.push(events::Event::DiscardFunding {
1545615468
channel_id: chan.context().channel_id(),
@@ -18123,6 +18135,7 @@ impl<
1812318135
user_channel_id: chan.context.get_user_id(),
1812418136
abandoned_funding_txo: splice_funding_failed.funding_txo,
1812518137
channel_type: splice_funding_failed.channel_type,
18138+
reason: events::NegotiationFailureReason::PeerDisconnected,
1812618139
},
1812718140
None,
1812818141
));

lightning/src/ln/functional_test_utils.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch
1717
use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync;
1818
use crate::events::bump_transaction::BumpTransactionEvent;
1919
use crate::events::{
20-
ClaimedHTLC, ClosureReason, Event, FundingInfo, HTLCHandlingFailureType, PaidBolt12Invoice,
21-
PathFailure, PaymentFailureReason, PaymentPurpose,
20+
ClaimedHTLC, ClosureReason, Event, FundingInfo, HTLCHandlingFailureType,
21+
NegotiationFailureReason, PaidBolt12Invoice, PathFailure, PaymentFailureReason, PaymentPurpose,
2222
};
2323
use crate::ln::chan_utils::{
2424
commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, TRUC_MAX_WEIGHT,
@@ -3243,13 +3243,14 @@ pub fn expect_splice_pending_event<'a, 'b, 'c, 'd>(
32433243
#[cfg(any(test, ldk_bench, feature = "_test_utils"))]
32443244
pub fn expect_splice_failed_events<'a, 'b, 'c, 'd>(
32453245
node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId,
3246-
funding_contribution: FundingContribution,
3246+
funding_contribution: FundingContribution, expected_reason: NegotiationFailureReason,
32473247
) {
32483248
let events = node.node.get_and_clear_pending_events();
32493249
assert_eq!(events.len(), 2);
32503250
match &events[0] {
3251-
Event::SpliceFailed { channel_id, .. } => {
3251+
Event::SpliceFailed { channel_id, reason, .. } => {
32523252
assert_eq!(*expected_channel_id, *channel_id);
3253+
assert_eq!(*reason, expected_reason);
32533254
},
32543255
_ => panic!("Unexpected event"),
32553256
}

0 commit comments

Comments
 (0)