Add NegotiationFailureReason to SpliceFailed event
What changed, and why it matters
This commit is a user-facing diagnostic improvement, not a security fix. It adds a 'reason' field to the SpliceFailed event so Lightning node operators can tell why a splice negotiation failed (for example, peer disconnected, feerate too low, or counterparty aborted). It changes no cryptographic checks, access controls, or network behavior. The only code-level risk is a minor serialization compatibility change: old stored events without the new field default to 'Unknown', and the persistence reload path defaults to 'PeerDisconnected'.
No security action required. Treat as a normal feature/API improvement. Reviewers may want to confirm that the serialization default choices (Unknown on missing field, PeerDisconnected on persistence reload) are acceptable for downstream consumers.
Security signals we found
No security-relevant behavioral change
Public API addition only (new enum and event field)
Serialization backward-compatibility handled via default values
No cryptographic, signature, or access-control modifications
No memory-safety or concurrency changes
Evidence from the diff
The patch introduces a new public enum NegotiationFailureReason and threads it through Event::SpliceFailed. It updates QuiescentError::FailSplice to carry the reason, sets reasons at each ChannelManager emission site based on triggering context, and adds TLV serialization (type 11, upgradable_option) with Unknown as the default for legacy data. It also adjusts one tx_abort message string. There are no changes to permission models, signature validation, transaction parsing, or cryptographic operations. The commit is purely informational/API ergonomics.
Changed components
lightning/src/events/mod.rslightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/functional_test_utils.rslightning/src/ln/splicing_tests.rsInspect captured patch +308 / −44
diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs
index 73c4a39..0c99ee0 100644
--- a/lightning/src/events/mod.rs
+++ b/lightning/src/events/mod.rs
@@ -99,6 +99,110 @@ 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::ContributionInvalid
+ | Self::FeeRateTooLow => true,
+ Self::CounterpartyAborted { .. }
+ | Self::NegotiationError { .. }
+ | 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,
+ (1, Unknown) => {},
+ (3, PeerDisconnected) => {},
+ (5, CounterpartyAborted) => {
+ (1, msg, required),
+ },
+ (7, NegotiationError) => {
+ (1, msg, required),
+ },
+ (9, ContributionInvalid) => {},
+ (11, LocallyAbandoned) => {},
+ (13, ChannelClosing) => {},
+ (15, 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)]
@@ -1586,6 +1690,8 @@ pub enum Event {
abandoned_funding_txo: Option<OutPoint>,
/// The features that this channel will operate with, if available.
channel_type: Option<ChannelTypeFeatures>,
+ /// The reason the splice negotiation failed.
+ reason: NegotiationFailureReason,
},
/// Used to indicate to the user that they can abandon the funding transaction and recycle the
/// inputs for another purpose.
@@ -2379,6 +2485,7 @@ impl Writeable for Event {
ref counterparty_node_id,
ref abandoned_funding_txo,
ref channel_type,
+ ref reason,
} => {
52u8.write(writer)?;
write_tlv_fields!(writer, {
@@ -2387,6 +2494,7 @@ impl Writeable for Event {
(5, user_channel_id, required),
(7, counterparty_node_id, required),
(9, abandoned_funding_txo, option),
+ (11, reason, required),
});
},
// Note that, going forward, all new events must only write data inside of
@@ -3031,6 +3139,7 @@ impl MaybeReadable for Event {
(5, user_channel_id, required),
(7, counterparty_node_id, required),
(9, abandoned_funding_txo, option),
+ (11, reason, upgradable_option),
});
Ok(Some(Event::SpliceFailed {
@@ -3039,6 +3148,7 @@ impl MaybeReadable for Event {
counterparty_node_id: counterparty_node_id.0.unwrap(),
abandoned_funding_txo,
channel_type,
+ reason: reason.unwrap_or(NegotiationFailureReason::Unknown),
}))
};
f()
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 8c74fa6..ad643a1 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -37,7 +37,7 @@ use crate::chain::channelmonitor::{
};
use crate::chain::transaction::{OutPoint, TransactionData};
use crate::chain::BlockLocator;
-use crate::events::{ClosureReason, FundingInfo};
+use crate::events::{ClosureReason, FundingInfo, NegotiationFailureReason};
use crate::ln::chan_utils;
use crate::ln::chan_utils::{
get_commitment_transaction_number_obscure_factor, max_htlcs, second_stage_tx_fees_sat,
@@ -3192,7 +3192,17 @@ pub(crate) enum QuiescentAction {
pub(super) enum QuiescentError {
DoNothing,
DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<bitcoin::TxOut> },
- FailSplice(SpliceFundingFailed),
+ FailSplice(SpliceFundingFailed, NegotiationFailureReason),
+}
+
+impl QuiescentError {
+ fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self {
+ match self {
+ QuiescentError::FailSplice(_, ref mut r) => *r = reason,
+ _ => debug_assert!(false, "Expected FailSplice variant"),
+ }
+ self
+ }
}
pub(crate) enum StfuResponse {
@@ -7155,9 +7165,10 @@ where
fn quiescent_action_into_error(&self, action: QuiescentAction) -> QuiescentError {
match action {
- QuiescentAction::Splice { contribution, .. } => {
- QuiescentError::FailSplice(self.splice_funding_failed_for(contribution))
- },
+ QuiescentAction::Splice { contribution, .. } => QuiescentError::FailSplice(
+ self.splice_funding_failed_for(contribution),
+ NegotiationFailureReason::Unknown,
+ ),
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
QuiescentAction::DoNothing => QuiescentError::DoNothing,
}
@@ -7166,7 +7177,7 @@ where
fn abandon_quiescent_action(&mut self) -> Option<SpliceFundingFailed> {
let action = self.quiescent_action.take()?;
match self.quiescent_action_into_error(action) {
- QuiescentError::FailSplice(failed) => Some(failed),
+ QuiescentError::FailSplice(failed, _) => Some(failed),
#[cfg(any(test, fuzzing, feature = "_test_utils"))]
QuiescentError::DoNothing => None,
_ => {
@@ -10446,7 +10457,7 @@ where
tx_abort = Some(msgs::TxAbort {
channel_id: self.context.channel_id(),
data:
- "No active signing session. The associated funding transaction may have already been broadcast.".as_bytes().to_vec() });
+ "Signing was not completed for this funding transaction; it may be forgotten.".as_bytes().to_vec() });
}
}
if let Some(funding_txid) = retransmit_funding_commit_sig {
@@ -12652,7 +12663,10 @@ where
)
{
log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e);
- return Err(QuiescentError::FailSplice(self.splice_funding_failed_for(contribution)));
+ return Err(QuiescentError::FailSplice(
+ self.splice_funding_failed_for(contribution),
+ NegotiationFailureReason::ContributionInvalid,
+ ));
}
if let Some(pending_splice) = self.pending_splice.as_ref() {
@@ -12668,6 +12682,7 @@ where
);
return Err(QuiescentError::FailSplice(
self.splice_funding_failed_for(contribution),
+ NegotiationFailureReason::FeeRateTooLow,
));
}
}
@@ -14165,9 +14180,18 @@ where
) -> Result<Option<msgs::Stfu>, QuiescentError> {
log_debug!(logger, "Attempting to initiate quiescence");
+ // TODO: NegotiationFailureReason is splice-specific, but propose_quiescence is
+ // generic. The reason should be selected by the caller, but it currently can't
+ // distinguish why quiescence failed. Revisit when a second quiescent protocol is added.
if !self.context.is_usable() {
+ debug_assert!(
+ self.context.channel_state.is_local_shutdown_sent()
+ || self.context.channel_state.is_remote_shutdown_sent(),
+ "splice_channel should have prevented reaching propose_quiescence on a non-ready channel"
+ );
log_debug!(logger, "Channel is not in a usable state to propose quiescence");
- return Err(self.quiescent_action_into_error(action));
+ return Err(self.quiescent_action_into_error(action)
+ .with_negotiation_failure_reason(NegotiationFailureReason::ChannelClosing));
}
if self.quiescent_action.is_some() {
log_debug!(
@@ -14286,7 +14310,10 @@ where
self.context.channel_id(),
e,
)),
- QuiescentError::FailSplice(failed),
+ QuiescentError::FailSplice(
+ failed,
+ NegotiationFailureReason::ContributionInvalid,
+ ),
));
}
let prior_contribution = contribution.clone();
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 6448659..ef2ce9a 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -4169,6 +4169,7 @@ impl<
user_channel_id: chan.context().get_user_id(),
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::ChannelClosing,
},
None,
));
@@ -4475,6 +4476,7 @@ impl<
user_channel_id: shutdown_res.user_channel_id,
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::ChannelClosing,
},
None,
));
@@ -4981,6 +4983,7 @@ impl<
user_channel_id: chan.context.get_user_id(),
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::LocallyAbandoned,
},
None,
));
@@ -6673,12 +6676,15 @@ impl<
));
}
},
- QuiescentError::FailSplice(SpliceFundingFailed {
- funding_txo,
- channel_type,
- contributed_inputs,
- contributed_outputs,
- }) => {
+ QuiescentError::FailSplice(
+ SpliceFundingFailed {
+ funding_txo,
+ channel_type,
+ contributed_inputs,
+ contributed_outputs,
+ },
+ reason,
+ ) => {
let pending_events = &mut self.pending_events.lock().unwrap();
pending_events.push_back((
events::Event::SpliceFailed {
@@ -6687,6 +6693,7 @@ impl<
user_channel_id,
abandoned_funding_txo: funding_txo,
channel_type,
+ reason,
},
None,
));
@@ -6840,7 +6847,7 @@ impl<
"Channel {} already has a pending funding contribution",
channel_id,
),
- QuiescentError::FailSplice(_) => format!(
+ QuiescentError::FailSplice(..) => format!(
"Channel {} cannot accept funding contribution",
channel_id,
),
@@ -11983,6 +11990,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
user_channel_id,
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type.clone(),
+ reason: events::NegotiationFailureReason::NegotiationError {
+ msg: format!("{:?}", err.err),
+ },
},
None,
));
@@ -12319,6 +12329,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
user_channel_id: chan_entry.get().context().get_user_id(),
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::CounterpartyAborted {
+ msg: UntrustedString(
+ String::from_utf8_lossy(&msg.data).to_string(),
+ ),
+ },
},
None,
));
@@ -12467,6 +12482,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
user_channel_id: chan.context().get_user_id(),
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::ChannelClosing,
},
None,
));
@@ -15543,6 +15559,7 @@ impl<
user_channel_id: chan.context().get_user_id(),
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::PeerDisconnected,
});
splice_failed_events.push(events::Event::DiscardFunding {
channel_id: chan.context().channel_id(),
@@ -18171,6 +18188,7 @@ impl<
user_channel_id: chan.context.get_user_id(),
abandoned_funding_txo: splice_funding_failed.funding_txo,
channel_type: splice_funding_failed.channel_type,
+ reason: events::NegotiationFailureReason::PeerDisconnected,
},
None,
));
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index b48d76d..b8ef589 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -19,8 +19,8 @@ use crate::chain::{BlockLocator, ChannelMonitorUpdateStatus, Confirm, Listen, Wa
use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync;
use crate::events::bump_transaction::BumpTransactionEvent;
use crate::events::{
- ClaimedHTLC, ClosureReason, Event, FundingInfo, HTLCHandlingFailureType, PaidBolt12Invoice,
- PathFailure, PaymentFailureReason, PaymentPurpose,
+ ClaimedHTLC, ClosureReason, Event, FundingInfo, HTLCHandlingFailureType,
+ NegotiationFailureReason, PaidBolt12Invoice, PathFailure, PaymentFailureReason, PaymentPurpose,
};
use crate::ln::chan_utils::{
commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, TRUC_MAX_WEIGHT,
@@ -3232,13 +3232,14 @@ pub fn expect_splice_pending_event<'a, 'b, 'c, 'd>(
#[cfg(any(test, ldk_bench, feature = "_test_utils"))]
pub fn expect_splice_failed_events<'a, 'b, 'c, 'd>(
node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId,
- funding_contribution: FundingContribution,
+ funding_contribution: FundingContribution, expected_reason: NegotiationFailureReason,
) {
let events = node.node.get_and_clear_pending_events();
assert_eq!(events.len(), 2);
match &events[0] {
- Event::SpliceFailed { channel_id, .. } => {
+ Event::SpliceFailed { channel_id, reason, .. } => {
assert_eq!(*expected_channel_id, *channel_id);
+ assert_eq!(expected_reason, *reason);
},
_ => panic!("Unexpected event"),
}
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 9a38139..34b51e2 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -13,7 +13,9 @@ use crate::chain::chaininterface::{FundingPurpose, TransactionType, FEERATE_FLOO
use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS};
use crate::chain::transaction::OutPoint;
use crate::chain::ChannelMonitorUpdateStatus;
-use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType};
+use crate::events::{
+ ClosureReason, Event, FundingInfo, HTLCHandlingFailureType, NegotiationFailureReason,
+};
use crate::ln::chan_utils;
use crate::ln::channel::{
ANCHOR_OUTPUT_VALUE_SATOSHI, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY,
@@ -28,6 +30,7 @@ use crate::ln::outbound_payment::RecipientOnionFields;
use crate::ln::types::ChannelId;
use crate::routing::router::{PaymentParameters, RouteParameters};
use crate::types::features::ChannelTypeFeatures;
+use crate::types::string::UntrustedString;
use crate::util::config::UserConfig;
use crate::util::errors::APIError;
use crate::util::ser::Writeable;
@@ -252,7 +255,12 @@ pub fn initiate_splice_out<'a, 'b, 'c, 'd>(
) {
Ok(()) => Ok(funding_contribution),
Err(e) => {
- expect_splice_failed_events(initiator, &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ initiator,
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::ContributionInvalid,
+ );
Err(e)
},
}
@@ -884,7 +892,12 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
nodes[1].node.peer_disconnected(node_id_0);
}
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_args.send_channel_ready = (true, true);
@@ -935,7 +948,12 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
nodes[1].node.peer_disconnected(node_id_0);
}
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_args.send_channel_ready = (true, true);
@@ -1017,7 +1035,17 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) {
let tx_abort = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1);
nodes[1].node.handle_tx_abort(node_id_0, &tx_abort);
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::CounterpartyAborted {
+ msg: UntrustedString(
+ "Signing was not completed for this funding transaction; it may be forgotten."
+ .to_string(),
+ ),
+ },
+ );
// Attempt a splice negotiation that completes, (i.e. `tx_signatures` are exchanged). Reconnecting
// should not abort the negotiation or reset the splice state.
@@ -1101,7 +1129,12 @@ fn test_config_reject_inbound_splices() {
nodes[0].node.peer_disconnected(node_id_1);
nodes[1].node.peer_disconnected(node_id_0);
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_args.send_channel_ready = (true, true);
@@ -2696,7 +2729,14 @@ fn fail_splice_on_interactive_tx_error() {
get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator);
initiator.node.handle_tx_add_input(node_id_acceptor, &tx_add_input);
- expect_splice_failed_events(initiator, &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ initiator,
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::NegotiationError {
+ msg: "Abort: Parity for `serial_id` was incorrect".to_string(),
+ },
+ );
// We exit quiescence upon sending `tx_abort`, so we should see the holding cell be immediately
// freed.
@@ -2767,7 +2807,12 @@ fn fail_splice_on_tx_abort() {
let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator);
initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort);
- expect_splice_failed_events(initiator, &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ initiator,
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::CounterpartyAborted { msg: UntrustedString(String::new()) },
+ );
// We exit quiescence upon receiving `tx_abort`, so we should see our `tx_abort` echo and the
// holding cell be immediately freed.
@@ -2863,7 +2908,16 @@ fn fail_splice_on_tx_complete_error() {
};
initiator.node.handle_tx_abort(node_id_acceptor, tx_abort);
- expect_splice_failed_events(initiator, &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ initiator,
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::CounterpartyAborted {
+ msg: UntrustedString(
+ "Total value of outputs exceeds total value of inputs".to_string(),
+ ),
+ },
+ );
let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor);
acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort);
@@ -3153,8 +3207,9 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool, pending_
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, .. } => {
assert_eq!(*cid, channel_id);
+ assert_eq!(*reason, NegotiationFailureReason::ChannelClosing);
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
@@ -3173,7 +3228,12 @@ fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool, pending_
other => panic!("Expected DiscardFunding with Contribution, got {:?}", other),
}
} else {
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::ChannelClosing,
+ );
}
let _ = get_event_msg!(closee_node, MessageSendEvent::SendShutdown, closer_node_id);
}
@@ -4146,7 +4206,12 @@ fn test_funding_contributed_channel_shutdown() {
})
);
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::ChannelClosing,
+ );
}
#[test]
@@ -4327,7 +4392,12 @@ fn do_test_splice_pending_htlcs(config: UserConfig) {
let reconnect_args = ReconnectArgs::new(initiator, acceptor);
reconnect_nodes(reconnect_args);
- expect_splice_failed_events(initiator, &channel_id, contribution);
+ expect_splice_failed_events(
+ initiator,
+ &channel_id,
+ contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
// 4) Try again with the additional satoshi removed from the splice-out message, and check that it passes
// validation on the receiver's side.
@@ -4362,7 +4432,12 @@ fn do_test_splice_pending_htlcs(config: UserConfig) {
nodes[1].node.peer_disconnected(node_id_0);
let reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_nodes(reconnect_args);
- expect_splice_failed_events(&nodes[1], &channel_id, contribution);
+ expect_splice_failed_events(
+ &nodes[1],
+ &channel_id,
+ contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
let details = &nodes[1].node.list_channels()[0];
let expected_outbound_htlc_max =
(pre_splice_balance.to_sat() - details.unspendable_punishment_reserve.unwrap()) * 1000;
@@ -4513,7 +4588,12 @@ fn test_splice_acceptor_disconnect_emits_events() {
nodes[1].node.peer_disconnected(node_id_0);
// The initiator should get SpliceFailed + DiscardFunding.
- expect_splice_failed_events(&nodes[0], &channel_id, node_0_funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ node_0_funding_contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
// The acceptor should also get SpliceFailed + DiscardFunding with its contributions
// so it can reclaim its UTXOs. The contribution is feerate-adjusted by handle_splice_init,
@@ -4521,7 +4601,10 @@ fn test_splice_acceptor_disconnect_emits_events() {
let events = nodes[1].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, .. } => assert_eq!(*cid, channel_id),
+ Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ assert_eq!(*cid, channel_id);
+ assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ },
other => panic!("Expected SpliceFailed, got {:?}", other),
}
match &events[1] {
@@ -6444,7 +6527,10 @@ fn test_splice_rbf_acceptor_contributes_then_disconnects() {
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, .. } => assert_eq!(*cid, channel_id),
+ Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ assert_eq!(*cid, channel_id);
+ assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ },
other => panic!("Expected SpliceFailed, got {:?}", other),
}
match &events[1] {
@@ -6523,8 +6609,9 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, .. } => {
+ Event::SpliceFailed { channel_id: cid, reason, .. } => {
assert_eq!(*cid, channel_id);
+ assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
},
other => panic!("Expected SpliceFailed, got {:?}", other),
}
@@ -6564,7 +6651,10 @@ fn test_splice_rbf_disconnect_filters_prior_contributions() {
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 2, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, .. } => assert_eq!(*cid, channel_id),
+ Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ assert_eq!(*cid, channel_id);
+ assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected);
+ },
other => panic!("Expected SpliceFailed, got {:?}", other),
}
match &events[1] {
@@ -7101,7 +7191,12 @@ fn test_splice_revalidation_at_quiescence() {
assert_eq!(msg_events.len(), 1, "{msg_events:?}");
assert!(matches!(msg_events[0], MessageSendEvent::HandleError { .. }));
- expect_splice_failed_events(&nodes[0], &channel_id, contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ contribution,
+ NegotiationFailureReason::ContributionInvalid,
+ );
}
#[test]
@@ -7351,7 +7446,10 @@ fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() {
let events = nodes[0].node.get_and_clear_pending_events();
assert_eq!(events.len(), 1, "{events:?}");
match &events[0] {
- Event::SpliceFailed { channel_id: cid, .. } => assert_eq!(*cid, channel_id),
+ Event::SpliceFailed { channel_id: cid, reason, .. } => {
+ assert_eq!(*cid, channel_id);
+ assert_eq!(*reason, NegotiationFailureReason::FeeRateTooLow);
+ },
other => panic!("Expected SpliceFailed, got {:?}", other),
}
}
@@ -7447,7 +7545,12 @@ fn test_no_disconnect_after_splice_aborted() {
// Abort the splice, which should clear the timer when exiting quiescence.
nodes[0].node.abandon_splice(&channel_id, &node_id_1).unwrap();
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::LocallyAbandoned,
+ );
let msg_events = nodes[0].node.get_and_clear_pending_msg_events();
let tx_abort = msg_events
@@ -7519,7 +7622,12 @@ fn test_no_disconnect_after_quiescence_on_reconnect() {
nodes[0].node.peer_disconnected(node_id_1);
nodes[1].node.peer_disconnected(node_id_0);
- expect_splice_failed_events(&nodes[0], &channel_id, funding_contribution);
+ expect_splice_failed_events(
+ &nodes[0],
+ &channel_id,
+ funding_contribution,
+ NegotiationFailureReason::PeerDisconnected,
+ );
let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]);
reconnect_args.send_channel_ready = (true, true);
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.