Skip to content

Commit 3ca4f3d

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 8d00139 commit 3ca4f3d

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,
@@ -3179,7 +3179,17 @@ pub(crate) enum QuiescentAction {
31793179
pub(super) enum QuiescentError {
31803180
DoNothing,
31813181
DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<bitcoin::TxOut> },
3182-
FailSplice(SpliceFundingFailed),
3182+
FailSplice(SpliceFundingFailed, NegotiationFailureReason),
3183+
}
3184+
3185+
impl QuiescentError {
3186+
fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
3187+
match self {
3188+
QuiescentError::FailSplice(_, ref mut r) => *r = reason,
3189+
_ => debug_assert!(false, "Expected FailSplice variant"),
3190+
}
3191+
self
3192+
}
31833193
}
31843194

31853195
pub(crate) enum StfuResponse {
@@ -6834,9 +6844,10 @@ where
68346844

68356845
fn quiescent_action_into_error(&self, action: QuiescentAction) -> QuiescentError {
68366846
match action {
6837-
QuiescentAction::Splice { contribution, .. } => {
6838-
QuiescentError::FailSplice(self.splice_funding_failed_for(contribution))
6839-
},
6847+
QuiescentAction::Splice { contribution, .. } => QuiescentError::FailSplice(
6848+
self.splice_funding_failed_for(contribution),
6849+
NegotiationFailureReason::Unknown,
6850+
),
68406851
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
68416852
QuiescentAction::DoNothing => QuiescentError::DoNothing,
68426853
}
@@ -6845,7 +6856,7 @@ where
68456856
fn abandon_quiescent_action(&mut self) -> Option<SpliceFundingFailed> {
68466857
let action = self.quiescent_action.take()?;
68476858
match self.quiescent_action_into_error(action) {
6848-
QuiescentError::FailSplice(failed) => Some(failed),
6859+
QuiescentError::FailSplice(failed, _) => Some(failed),
68496860
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
68506861
QuiescentError::DoNothing => None,
68516862
_ => {
@@ -12243,7 +12254,10 @@ where
1224312254
}) {
1224412255
log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e);
1224512256

12246-
return Err(QuiescentError::FailSplice(self.splice_funding_failed_for(contribution)));
12257+
return Err(QuiescentError::FailSplice(
12258+
self.splice_funding_failed_for(contribution),
12259+
NegotiationFailureReason::ContributionInvalid,
12260+
));
1224712261
}
1224812262

1224912263
if let Some(pending_splice) = self.pending_splice.as_ref() {
@@ -12259,6 +12273,7 @@ where
1225912273
);
1226012274
return Err(QuiescentError::FailSplice(
1226112275
self.splice_funding_failed_for(contribution),
12276+
NegotiationFailureReason::FeeRateTooLow,
1226212277
));
1226312278
}
1226412279
}
@@ -13703,7 +13718,8 @@ where
1370313718

1370413719
if !self.context.is_usable() {
1370513720
log_debug!(logger, "Channel is not in a usable state to propose quiescence");
13706-
return Err(self.quiescent_action_into_error(action));
13721+
return Err(self.quiescent_action_into_error(action)
13722+
.with_negotiation_failure_reason(NegotiationFailureReason::ChannelNotReady));
1370713723
}
1370813724
if self.quiescent_action.is_some() {
1370913725
log_debug!(
@@ -13820,7 +13836,10 @@ where
1382013836
self.context.channel_id(),
1382113837
e,
1382213838
)),
13823-
QuiescentError::FailSplice(failed),
13839+
QuiescentError::FailSplice(
13840+
failed,
13841+
NegotiationFailureReason::ContributionInvalid,
13842+
),
1382413843
));
1382513844
}
1382613845
let prior_contribution = contribution.clone();

lightning/src/ln/channelmanager.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4011,6 +4011,7 @@ impl<
40114011
user_channel_id: chan.context().get_user_id(),
40124012
abandoned_funding_txo: splice_funding_failed.funding_txo,
40134013
channel_type: splice_funding_failed.channel_type,
4014+
reason: events::NegotiationFailureReason::ChannelClosing,
40144015
},
40154016
None,
40164017
));
@@ -4317,6 +4318,7 @@ impl<
43174318
user_channel_id: shutdown_res.user_channel_id,
43184319
abandoned_funding_txo: splice_funding_failed.funding_txo,
43194320
channel_type: splice_funding_failed.channel_type,
4321+
reason: events::NegotiationFailureReason::ChannelClosing,
43204322
},
43214323
None,
43224324
));
@@ -4823,6 +4825,7 @@ impl<
48234825
user_channel_id: chan.context.get_user_id(),
48244826
abandoned_funding_txo: splice_funding_failed.funding_txo,
48254827
channel_type: splice_funding_failed.channel_type,
4828+
reason: events::NegotiationFailureReason::LocallyAbandoned,
48264829
},
48274830
None,
48284831
));
@@ -6505,12 +6508,15 @@ impl<
65056508
));
65066509
}
65076510
},
6508-
QuiescentError::FailSplice(SpliceFundingFailed {
6509-
funding_txo,
6510-
channel_type,
6511-
contributed_inputs,
6512-
contributed_outputs,
6513-
}) => {
6511+
QuiescentError::FailSplice(
6512+
SpliceFundingFailed {
6513+
funding_txo,
6514+
channel_type,
6515+
contributed_inputs,
6516+
contributed_outputs,
6517+
},
6518+
reason,
6519+
) => {
65146520
let pending_events = &mut self.pending_events.lock().unwrap();
65156521
pending_events.push_back((
65166522
events::Event::SpliceFailed {
@@ -6519,6 +6525,7 @@ impl<
65196525
user_channel_id,
65206526
abandoned_funding_txo: funding_txo,
65216527
channel_type,
6528+
reason,
65226529
},
65236530
None,
65246531
));
@@ -6660,7 +6667,7 @@ impl<
66606667
"Channel {} already has a pending funding contribution",
66616668
channel_id,
66626669
),
6663-
QuiescentError::FailSplice(_) => format!(
6670+
QuiescentError::FailSplice(..) => format!(
66646671
"Channel {} cannot accept funding contribution",
66656672
channel_id,
66666673
),
@@ -11708,6 +11715,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1170811715
user_channel_id: channel.context().get_user_id(),
1170911716
abandoned_funding_txo: splice_funding_failed.funding_txo,
1171011717
channel_type: splice_funding_failed.channel_type.clone(),
11718+
reason: events::NegotiationFailureReason::NegotiationError,
1171111719
},
1171211720
None,
1171311721
));
@@ -11867,6 +11875,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1186711875
user_channel_id: chan.context().get_user_id(),
1186811876
abandoned_funding_txo: splice_funding_failed.funding_txo,
1186911877
channel_type: splice_funding_failed.channel_type.clone(),
11878+
reason: events::NegotiationFailureReason::NegotiationError,
1187011879
},
1187111880
None,
1187211881
));
@@ -12037,6 +12046,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1203712046
user_channel_id: chan_entry.get().context().get_user_id(),
1203812047
abandoned_funding_txo: splice_funding_failed.funding_txo,
1203912048
channel_type: splice_funding_failed.channel_type,
12049+
reason: events::NegotiationFailureReason::CounterpartyAborted,
1204012050
},
1204112051
None,
1204212052
));
@@ -12185,6 +12195,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
1218512195
user_channel_id: chan.context().get_user_id(),
1218612196
abandoned_funding_txo: splice_funding_failed.funding_txo,
1218712197
channel_type: splice_funding_failed.channel_type,
12198+
reason: events::NegotiationFailureReason::ChannelClosing,
1218812199
},
1218912200
None,
1219012201
));
@@ -15293,6 +15304,7 @@ impl<
1529315304
user_channel_id: chan.context().get_user_id(),
1529415305
abandoned_funding_txo: splice_funding_failed.funding_txo,
1529515306
channel_type: splice_funding_failed.channel_type,
15307+
reason: events::NegotiationFailureReason::PeerDisconnected,
1529615308
});
1529715309
splice_failed_events.push(events::Event::DiscardFunding {
1529815310
channel_id: chan.context().channel_id(),
@@ -17943,6 +17955,7 @@ impl<
1794317955
user_channel_id: chan.context.get_user_id(),
1794417956
abandoned_funding_txo: splice_funding_failed.funding_txo,
1794517957
channel_type: splice_funding_failed.channel_type,
17958+
reason: events::NegotiationFailureReason::PeerDisconnected,
1794617959
},
1794717960
None,
1794817961
));

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,
@@ -3238,13 +3238,14 @@ pub fn expect_splice_pending_event<'a, 'b, 'c, 'd>(
32383238
#[cfg(any(test, ldk_bench, feature = "_test_utils"))]
32393239
pub fn expect_splice_failed_events<'a, 'b, 'c, 'd>(
32403240
node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId,
3241-
funding_contribution: FundingContribution,
3241+
funding_contribution: FundingContribution, expected_reason: NegotiationFailureReason,
32423242
) {
32433243
let events = node.node.get_and_clear_pending_events();
32443244
assert_eq!(events.len(), 2);
32453245
match &events[0] {
3246-
Event::SpliceFailed { channel_id, .. } => {
3246+
Event::SpliceFailed { channel_id, reason, .. } => {
32473247
assert_eq!(*expected_channel_id, *channel_id);
3248+
assert_eq!(*reason, expected_reason);
32483249
},
32493250
_ => panic!("Unexpected event"),
32503251
}

0 commit comments

Comments
 (0)