Emit SpliceFailed event for interactive-tx failures
What changed, and why it matters
This change improves how the Lightning Dev Kit (LDK) node tells its user when a splice funding negotiation fails during interactive transaction construction. Previously, failures were mostly silent at the application level, meaning users might not know they could reclaim UTXOs they had contributed. Now the node emits a new SpliceFailed event containing the contributed inputs and outputs so wallets can act. It also changes how some protocol errors are handled: unexpected interactive-tx messages now trigger a warning/disconnect instead of being treated as an internal abort. This is a reliability/usability fix rather than a critical remote-exploitable vulnerability.
Review the new SpliceFailed event handling in wallet integrations to ensure users are notified and can reclaim UTXOs. Monitor for any edge cases where the initiator check might omit events for non-initiators who also contributed outputs. No urgent security patch is indicated.
Security signals we found
New event emission on negotiation failure reduces risk of user funds being stranded due to lack of notification
Unexpected interactive-tx messages now treated as warning/disconnect rather than internal abort
Persistence now occurs on interactive-tx handler errors, improving state consistency after failures
No evidence of memory safety issues, cryptographic flaws, or remote code execution paths
Evidence from the diff
The commit introduces Event::SpliceFailed and wires it into channelmanager and channel state handling. fail_interactive_tx_negotiation now returns (ChannelError::Abort, Option
Changed components
lightning/src/ln/channel.rslightning/src/ln/channelmanager.rslightning/src/ln/interactivetxs.rslightning/src/ln/funding.rslightning/src/ln/splicing_tests.rslightning/src/ln/functional_test_utils.rsInspect captured patch +494 / −126
diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs
index 9142650..f4687b2 100644
--- a/lightning/src/ln/channel.rs
+++ b/lightning/src/ln/channel.rs
@@ -920,6 +920,7 @@ pub(super) enum ChannelError {
Ignore(String),
Warn(String),
WarnAndDisconnect(String),
+ Abort(AbortReason),
Close((String, ClosureReason)),
SendError(String),
}
@@ -932,6 +933,7 @@ impl fmt::Debug for ChannelError {
&ChannelError::WarnAndDisconnect(ref e) => {
write!(f, "Disconnecting with warning: {}", e)
},
+ &ChannelError::Abort(ref reason) => write!(f, "Abort: {}", reason),
&ChannelError::Close((ref e, _)) => write!(f, "Close: {}", e),
&ChannelError::SendError(ref e) => write!(f, "Not Found: {}", e),
}
@@ -944,6 +946,7 @@ impl fmt::Display for ChannelError {
&ChannelError::Ignore(ref e) => write!(f, "{}", e),
&ChannelError::Warn(ref e) => write!(f, "{}", e),
&ChannelError::WarnAndDisconnect(ref e) => write!(f, "{}", e),
+ &ChannelError::Abort(ref reason) => write!(f, "{}", reason),
&ChannelError::Close((ref e, _)) => write!(f, "{}", e),
&ChannelError::SendError(ref e) => write!(f, "{}", e),
}
@@ -1680,110 +1683,132 @@ where
fn fail_interactive_tx_negotiation<L: Deref>(
&mut self, reason: AbortReason, logger: &L,
- ) -> msgs::TxAbort
+ ) -> (ChannelError, Option<SpliceFundingFailed>)
where
L::Target: Logger,
{
let logger = WithChannelContext::from(logger, &self.context(), None);
log_info!(logger, "Failed interactive transaction negotiation: {reason}");
- match &mut self.phase {
+ let splice_funding_failed = match &mut self.phase {
ChannelPhase::Undefined => unreachable!(),
- ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => {},
+ ChannelPhase::UnfundedOutboundV1(_) | ChannelPhase::UnfundedInboundV1(_) => None,
ChannelPhase::UnfundedV2(pending_v2_channel) => {
pending_v2_channel.interactive_tx_constructor.take();
+ None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state() {
- funded_channel.reset_pending_splice_state();
+ funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
+ None
}
},
};
- reason.into_tx_abort_msg(self.context().channel_id)
+ (ChannelError::Abort(reason), splice_funding_failed)
}
pub fn tx_add_input<L: Deref>(
&mut self, msg: &msgs::TxAddInput, logger: &L,
- ) -> Result<InteractiveTxMessageSend, msgs::TxAbort>
+ ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)>
where
L::Target: Logger,
{
match self.interactive_tx_constructor_mut() {
- Some(interactive_tx_constructor) => interactive_tx_constructor.handle_tx_add_input(msg),
- None => Err(AbortReason::InternalError(
- "Received unexpected interactive transaction negotiation message",
+ Some(interactive_tx_constructor) => interactive_tx_constructor
+ .handle_tx_add_input(msg)
+ .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)),
+ None => Err((
+ ChannelError::WarnAndDisconnect(
+ "Received unexpected interactive transaction negotiation message".to_owned(),
+ ),
+ None,
)),
}
- .map_err(|abort_reason| self.fail_interactive_tx_negotiation(abort_reason, logger))
}
pub fn tx_add_output<L: Deref>(
&mut self, msg: &msgs::TxAddOutput, logger: &L,
- ) -> Result<InteractiveTxMessageSend, msgs::TxAbort>
+ ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)>
where
L::Target: Logger,
{
match self.interactive_tx_constructor_mut() {
- Some(interactive_tx_constructor) => {
- interactive_tx_constructor.handle_tx_add_output(msg)
- },
- None => Err(AbortReason::InternalError(
- "Received unexpected interactive transaction negotiation message",
+ Some(interactive_tx_constructor) => interactive_tx_constructor
+ .handle_tx_add_output(msg)
+ .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)),
+ None => Err((
+ ChannelError::WarnAndDisconnect(
+ "Received unexpected interactive transaction negotiation message".to_owned(),
+ ),
+ None,
)),
}
- .map_err(|abort_reason| self.fail_interactive_tx_negotiation(abort_reason, logger))
}
pub fn tx_remove_input<L: Deref>(
&mut self, msg: &msgs::TxRemoveInput, logger: &L,
- ) -> Result<InteractiveTxMessageSend, msgs::TxAbort>
+ ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)>
where
L::Target: Logger,
{
match self.interactive_tx_constructor_mut() {
- Some(interactive_tx_constructor) => {
- interactive_tx_constructor.handle_tx_remove_input(msg)
- },
- None => Err(AbortReason::InternalError(
- "Received unexpected interactive transaction negotiation message",
+ Some(interactive_tx_constructor) => interactive_tx_constructor
+ .handle_tx_remove_input(msg)
+ .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)),
+ None => Err((
+ ChannelError::WarnAndDisconnect(
+ "Received unexpected interactive transaction negotiation message".to_owned(),
+ ),
+ None,
)),
}
- .map_err(|abort_reason| self.fail_interactive_tx_negotiation(abort_reason, logger))
}
pub fn tx_remove_output<L: Deref>(
&mut self, msg: &msgs::TxRemoveOutput, logger: &L,
- ) -> Result<InteractiveTxMessageSend, msgs::TxAbort>
+ ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)>
where
L::Target: Logger,
{
match self.interactive_tx_constructor_mut() {
- Some(interactive_tx_constructor) => {
- interactive_tx_constructor.handle_tx_remove_output(msg)
- },
- None => Err(AbortReason::InternalError(
- "Received unexpected interactive transaction negotiation message",
+ Some(interactive_tx_constructor) => interactive_tx_constructor
+ .handle_tx_remove_output(msg)
+ .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)),
+ None => Err((
+ ChannelError::WarnAndDisconnect(
+ "Received unexpected interactive transaction negotiation message".to_owned(),
+ ),
+ None,
)),
}
- .map_err(|abort_reason| self.fail_interactive_tx_negotiation(abort_reason, logger))
}
pub fn tx_complete<L: Deref>(
&mut self, msg: &msgs::TxComplete, logger: &L,
- ) -> Result<(Option<InteractiveTxMessageSend>, Option<msgs::CommitmentSigned>), msgs::TxAbort>
+ ) -> Result<
+ (Option<InteractiveTxMessageSend>, Option<msgs::CommitmentSigned>),
+ (ChannelError, Option<SpliceFundingFailed>),
+ >
where
L::Target: Logger,
{
let tx_complete_action = match self.interactive_tx_constructor_mut() {
- Some(interactive_tx_constructor) => interactive_tx_constructor.handle_tx_complete(msg),
- None => Err(AbortReason::InternalError(
- "Received unexpected interactive transaction negotiation message",
- )),
- }
- .map_err(|abort_reason| self.fail_interactive_tx_negotiation(abort_reason, logger))?;
+ Some(interactive_tx_constructor) => interactive_tx_constructor
+ .handle_tx_complete(msg)
+ .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger))?,
+ None => {
+ return Err((
+ ChannelError::WarnAndDisconnect(
+ "Received unexpected interactive transaction negotiation message"
+ .to_owned(),
+ ),
+ None,
+ ))
+ },
+ };
let (interactive_tx_msg_send, negotiation_complete) = match tx_complete_action {
HandleTxCompleteValue::SendTxMessage(interactive_tx_msg_send) => {
@@ -1834,8 +1859,13 @@ where
));
}
if funded_channel.should_reset_pending_splice_state() {
- let has_funding_negotiation = funded_channel.reset_pending_splice_state();
+ let has_funding_negotiation = funded_channel
+ .pending_splice
+ .as_ref()
+ .map(|pending_splice| pending_splice.funding_negotiation.is_some())
+ .unwrap_or(false);
debug_assert!(has_funding_negotiation);
+ funded_channel.reset_pending_splice_state();
true
} else {
// We were not tracking the pending funding negotiation state anymore, likely
@@ -1931,7 +1961,8 @@ where
interactive_tx_constructor,
} = funding_negotiation
{
- Some((funding, interactive_tx_constructor))
+ let is_initiator = interactive_tx_constructor.is_initiator();
+ Some((is_initiator, funding, interactive_tx_constructor))
} else {
// Replace the taken state for later error handling
pending_splice.funding_negotiation = Some(funding_negotiation);
@@ -1943,7 +1974,7 @@ where
"Got a tx_complete message in an invalid state",
)
})
- .and_then(|(mut funding, interactive_tx_constructor)| {
+ .and_then(|(is_initiator, mut funding, interactive_tx_constructor)| {
match chan.context.funding_tx_constructed(
&mut funding,
funding_outpoint,
@@ -1954,7 +1985,10 @@ where
Ok(commitment_signed) => {
// Advance the state
pending_splice.funding_negotiation =
- Some(FundingNegotiation::AwaitingSignatures { funding });
+ Some(FundingNegotiation::AwaitingSignatures {
+ is_initiator,
+ funding,
+ });
Ok((interactive_tx_constructor, commitment_signed))
},
Err(e) => {
@@ -2558,12 +2592,14 @@ enum FundingNegotiation {
},
AwaitingSignatures {
funding: FundingScope,
+ is_initiator: bool,
},
}
impl_writeable_tlv_based_enum_upgradable!(FundingNegotiation,
(0, AwaitingSignatures) => {
(1, funding, required),
+ (3, is_initiator, required),
},
unread_variants: AwaitingAck, ConstructingTransaction
);
@@ -2573,7 +2609,17 @@ impl FundingNegotiation {
match self {
FundingNegotiation::AwaitingAck { .. } => None,
FundingNegotiation::ConstructingTransaction { funding, .. } => Some(funding),
- FundingNegotiation::AwaitingSignatures { funding } => Some(funding),
+ FundingNegotiation::AwaitingSignatures { funding, .. } => Some(funding),
+ }
+ }
+
+ fn is_initiator(&self) -> bool {
+ match self {
+ FundingNegotiation::AwaitingAck { context } => context.is_initiator,
+ FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
+ interactive_tx_constructor.is_initiator()
+ },
+ FundingNegotiation::AwaitingSignatures { is_initiator, .. } => *is_initiator,
}
}
}
@@ -6614,12 +6660,15 @@ impl FundingNegotiationContext {
}
fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError {
+ let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs();
+ NegotiationError { reason, contributed_inputs, contributed_outputs }
+ }
+
+ fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) {
let contributed_inputs =
self.our_funding_inputs.into_iter().map(|input| input.utxo.outpoint).collect();
-
let contributed_outputs = self.our_funding_outputs;
-
- NegotiationError { reason, contributed_inputs, contributed_outputs }
+ (contributed_inputs, contributed_outputs)
}
}
@@ -6739,6 +6788,21 @@ pub struct SpliceFundingNegotiated {
pub channel_type: ChannelTypeFeatures,
}
+/// Information about a splice funding negotiation that has failed.
+pub struct SpliceFundingFailed {
+ /// The outpoint of the channel's splice funding transaction, if one was created.
+ pub funding_txo: Option<bitcoin::OutPoint>,
+
+ /// The features that this channel will operate with, if available.
+ pub channel_type: Option<ChannelTypeFeatures>,
+
+ /// UTXOs spent as inputs contributed to the splice transaction.
+ pub contributed_inputs: Vec<bitcoin::OutPoint>,
+
+ /// Outputs contributed to the splice transaction.
+ pub contributed_outputs: Vec<bitcoin::TxOut>,
+}
+
pub struct SpliceFundingPromotion {
pub funding_txo: OutPoint,
pub monitor_update: Option<ChannelMonitorUpdate>,
@@ -6813,19 +6877,53 @@ where
.unwrap_or(false)
}
- fn reset_pending_splice_state(&mut self) -> bool {
+ fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state());
debug_assert!(self.context.interactive_tx_signing_session.is_none());
self.context.channel_state.clear_quiescent();
- let has_funding_negotiation = self
+
+ let splice_funding_failed = self
.pending_splice
.as_mut()
.and_then(|pending_splice| pending_splice.funding_negotiation.take())
- .is_some();
+ .filter(|funding_negotiation| funding_negotiation.is_initiator())
+ .map(|funding_negotiation| {
+ let funding_txo = funding_negotiation
+ .as_funding()
+ .and_then(|funding| funding.get_funding_txo())
+ .map(|txo| txo.into_bitcoin_outpoint());
+
+ let channel_type = funding_negotiation
+ .as_funding()
+ .map(|funding| funding.get_channel_type().clone());
+
+ let (contributed_inputs, contributed_outputs) = match funding_negotiation {
+ FundingNegotiation::AwaitingAck { context } => {
+ context.into_contributed_inputs_and_outputs()
+ },
+ FundingNegotiation::ConstructingTransaction {
+ interactive_tx_constructor,
+ ..
+ } => interactive_tx_constructor.into_contributed_inputs_and_outputs(),
+ FundingNegotiation::AwaitingSignatures { .. } => {
+ debug_assert!(false);
+ (Vec::new(), Vec::new())
+ },
+ };
+
+ SpliceFundingFailed {
+ funding_txo,
+ channel_type,
+ contributed_inputs,
+ contributed_outputs,
+ }
+ });
+
if self.pending_funding().is_empty() {
self.pending_splice.take();
}
- has_funding_negotiation
+
+ splice_funding_failed
}
#[rustfmt::skip]
@@ -8665,7 +8763,7 @@ where
if let Some(pending_splice) = self.pending_splice.as_mut() {
self.context.channel_state.clear_quiescent();
- if let Some(FundingNegotiation::AwaitingSignatures { mut funding }) =
+ if let Some(FundingNegotiation::AwaitingSignatures { mut funding, .. }) =
pending_splice.funding_negotiation.take()
{
funding.funding_transaction = Some(funding_tx);
@@ -8713,7 +8811,11 @@ where
if signing_session.holder_tx_signatures().is_some() {
// Our `tx_signatures` either should've been the first time we processed them,
// or we're waiting for our counterparty to send theirs first.
- return Ok(FundingTxSigned { tx_signatures: None, funding_tx: None, splice_negotiated: None });
+ return Ok(FundingTxSigned {
+ tx_signatures: None,
+ funding_tx: None,
+ splice_negotiated: None,
+ });
}
signing_session
@@ -8721,7 +8823,11 @@ where
if Some(funding_txid_signed) == self.funding.get_funding_txid() {
// We may be handling a duplicate call and the funding was already locked so we
// no longer have the signing session present.
- return Ok(FundingTxSigned { tx_signatures: None, funding_tx: None, splice_negotiated: None });
+ return Ok(FundingTxSigned {
+ tx_signatures: None,
+ funding_tx: None,
+ splice_negotiated: None,
+ });
}
let err =
format!("Channel {} not expecting funding signatures", self.context.channel_id);
@@ -9600,7 +9706,7 @@ where
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref())
.and_then(|funding_negotiation| {
- if let FundingNegotiation::AwaitingSignatures { funding } = &funding_negotiation {
+ if let FundingNegotiation::AwaitingSignatures { funding, .. } = &funding_negotiation {
Some(funding)
} else {
None
diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs
index 9b0086c..46ca760 100644
--- a/lightning/src/ln/channelmanager.rs
+++ b/lightning/src/ln/channelmanager.rs
@@ -61,7 +61,8 @@ use crate::ln::channel::QuiescentAction;
use crate::ln::channel::{
self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, FundedChannel,
FundingTxSigned, InboundV1Channel, OutboundV1Channel, PendingV2Channel, ReconnectionMsg,
- ShutdownResult, StfuResponse, UpdateFulfillCommitFetch, WithChannelContext,
+ ShutdownResult, SpliceFundingFailed, StfuResponse, UpdateFulfillCommitFetch,
+ WithChannelContext,
};
use crate::ln::channel_state::ChannelDetails;
use crate::ln::funding::SpliceContribution;
@@ -931,6 +932,7 @@ struct MsgHandleErrInternal {
err: msgs::LightningError,
closes_channel: bool,
shutdown_finish: Option<(ShutdownResult, Option<msgs::ChannelUpdate>)>,
+ tx_abort: Option<msgs::TxAbort>,
}
impl MsgHandleErrInternal {
fn send_err_msg_no_close(err: String, channel_id: ChannelId) -> Self {
@@ -943,11 +945,12 @@ impl MsgHandleErrInternal {
},
closes_channel: false,
shutdown_finish: None,
+ tx_abort: None,
}
}
fn from_no_close(err: msgs::LightningError) -> Self {
- Self { err, closes_channel: false, shutdown_finish: None }
+ Self { err, closes_channel: false, shutdown_finish: None, tx_abort: None }
}
fn from_finish_shutdown(
@@ -967,10 +970,15 @@ impl MsgHandleErrInternal {
err: LightningError { err, action },
closes_channel: true,
shutdown_finish: Some((shutdown_res, channel_update)),
+ tx_abort: None,
}
}
fn from_chan_no_close(err: ChannelError, channel_id: ChannelId) -> Self {
+ let tx_abort = match &err {
+ &ChannelError::Abort(reason) => Some(reason.into_tx_abort_msg(channel_id)),
+ _ => None,
+ };
let err = match err {
ChannelError::Warn(msg) => LightningError {
err: msg.clone(),
@@ -988,6 +996,9 @@ impl MsgHandleErrInternal {
ChannelError::Ignore(msg) => {
LightningError { err: msg, action: msgs::ErrorAction::IgnoreError }
},
+ ChannelError::Abort(reason) => {
+ LightningError { err: reason.to_string(), action: msgs::ErrorAction::IgnoreError }
+ },
ChannelError::Close((msg, _)) | ChannelError::SendError(msg) => LightningError {
err: msg.clone(),
action: msgs::ErrorAction::SendErrorMessage {
@@ -995,7 +1006,7 @@ impl MsgHandleErrInternal {
},
},
};
- Self { err, closes_channel: false, shutdown_finish: None }
+ Self { err, closes_channel: false, shutdown_finish: None, tx_abort }
}
fn dont_send_error_message(&mut self) {
@@ -3210,7 +3221,7 @@ macro_rules! handle_error {
match $internal {
Ok(msg) => Ok(msg),
- Err(MsgHandleErrInternal { err, shutdown_finish, .. }) => {
+ Err(MsgHandleErrInternal { err, shutdown_finish, tx_abort, .. }) => {
let mut msg_event = None;
if let Some((shutdown_res, update_option)) = shutdown_finish {
@@ -3233,6 +3244,12 @@ macro_rules! handle_error {
}
if let msgs::ErrorAction::IgnoreError = err.action {
+ if let Some(tx_abort) = tx_abort {
+ msg_event = Some(MessageSendEvent::SendTxAbort {
+ node_id: $counterparty_node_id,
+ msg: tx_abort,
+ });
+ }
} else {
msg_event = Some(MessageSendEvent::HandleError {
node_id: $counterparty_node_id,
@@ -3330,6 +3347,9 @@ macro_rules! convert_channel_err {
ChannelError::Ignore(msg) => {
(false, MsgHandleErrInternal::from_chan_no_close(ChannelError::Ignore(msg), $channel_id))
},
+ ChannelError::Abort(reason) => {
+ (false, MsgHandleErrInternal::from_chan_no_close(ChannelError::Abort(reason), $channel_id))
+ },
ChannelError::Close((msg, reason)) => {
let (mut shutdown_res, chan_update) = $close(reason);
let logger = WithChannelContext::from(&$self.logger, &$chan.context(), None);
@@ -10270,11 +10290,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
}
fn internal_tx_msg<
- HandleTxMsgFn: Fn(&mut Channel<SP>) -> Result<InteractiveTxMessageSend, msgs::TxAbort>,
+ HandleTxMsgFn: Fn(
+ &mut Channel<SP>,
+ ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)>,
>(
&self, counterparty_node_id: &PublicKey, channel_id: ChannelId,
tx_msg_handler: HandleTxMsgFn,
- ) -> Result<(), MsgHandleErrInternal> {
+ ) -> Result<NotifyOption, MsgHandleErrInternal> {
let per_peer_state = self.per_peer_state.read().unwrap();
let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| {
debug_assert!(false);
@@ -10288,17 +10310,28 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
match peer_state.channel_by_id.entry(channel_id) {
hash_map::Entry::Occupied(mut chan_entry) => {
let channel = chan_entry.get_mut();
- let msg_send_event = match tx_msg_handler(channel) {
- Ok(msg_send) => msg_send.into_msg_send_event(*counterparty_node_id),
- Err(tx_abort) => {
- MessageSendEvent::SendTxAbort {
- node_id: *counterparty_node_id,
- msg: tx_abort,
+ match tx_msg_handler(channel) {
+ Ok(msg_send) => {
+ let msg_send_event = msg_send.into_msg_send_event(*counterparty_node_id);
+ peer_state.pending_msg_events.push(msg_send_event);
+ Ok(NotifyOption::SkipPersistHandleEvents)
+ },
+ Err((error, splice_funding_failed)) => {
+ if let Some(splice_funding_failed) = splice_funding_failed {
+ let pending_events = &mut self.pending_events.lock().unwrap();
+ pending_events.push_back((events::Event::SpliceFailed {
+ channel_id,
+ counterparty_node_id: *counterparty_node_id,
+ user_channel_id: channel.context().get_user_id(),
+ abandoned_funding_txo: splice_funding_failed.funding_txo,
+ channel_type: splice_funding_failed.channel_type.clone(),
+ contributed_inputs: splice_funding_failed.contributed_inputs,
+ contributed_outputs: splice_funding_failed.contributed_outputs,
+ }, None));
}
+ Err(MsgHandleErrInternal::from_chan_no_close(error, channel_id))
},
- };
- peer_state.pending_msg_events.push(msg_send_event);
- Ok(())
+ }
},
hash_map::Entry::Vacant(_) => {
Err(MsgHandleErrInternal::send_err_msg_no_close(format!(
@@ -10311,7 +10344,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
fn internal_tx_add_input(
&self, counterparty_node_id: PublicKey, msg: &msgs::TxAddInput,
- ) -> Result<(), MsgHandleErrInternal> {
+ ) -> Result<NotifyOption, MsgHandleErrInternal> {
self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| {
channel.tx_add_input(msg, &self.logger)
})
@@ -10319,7 +10352,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
fn internal_tx_add_output(
&self, counterparty_node_id: PublicKey, msg: &msgs::TxAddOutput,
- ) -> Result<(), MsgHandleErrInternal> {
+ ) -> Result<NotifyOption, MsgHandleErrInternal> {
self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| {
channel.tx_add_output(msg, &self.logger)
})
@@ -10327,7 +10360,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
fn internal_tx_remove_input(
&self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveInput,
- ) -> Result<(), MsgHandleErrInternal> {
+ ) -> Result<NotifyOption, MsgHandleErrInternal> {
self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| {
channel.tx_remove_input(msg, &self.logger)
})
@@ -10335,14 +10368,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
fn internal_tx_remove_output(
&self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveOutput,
- ) -> Result<(), MsgHandleErrInternal> {
+ ) -> Result<NotifyOption, MsgHandleErrInternal> {
self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| {
channel.tx_remove_output(msg, &self.logger)
})
}
#[rustfmt::skip]
- fn internal_tx_complete(&self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete) -> Result<(), MsgHandleErrInternal> {
+ fn internal_tx_complete(&self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete) -> Result<NotifyOption, MsgHandleErrInternal> {
let per_peer_state = self.per_peer_state.read().unwrap();
let peer_state_mutex = per_peer_state.get(&counterparty_node_id)
.ok_or_else(|| {
@@ -10358,6 +10391,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
let chan = chan_entry.get_mut();
match chan.tx_complete(msg, &self.logger) {
Ok((interactive_tx_msg_send, commitment_signed)) => {
+ let persist = if interactive_tx_msg_send.is_some() || commitment_signed.is_some() {
+ NotifyOption::SkipPersistHandleEvents
+ } else {
+ NotifyOption::SkipPersistNoEvents
+ };
if let Some(interactive_tx_msg_send) = interactive_tx_msg_send {
let msg_send_event = interactive_tx_msg_send.into_msg_send_event(counterparty_node_id);
peer_state.pending_msg_events.push(msg_send_event);
@@ -10376,15 +10414,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/
},
});
}
+ Ok(persist)
},
- Err(tx_abort) => {
- peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort {
- node_id: counterparty_node_id,
- msg: tx_abort,
- });
+ Err((error, splice_funding_failed)) => {
+ if let Some(splice_funding_failed) = splice_funding_failed {
+ let pending_events = &mut self.pending_events.lock().unwrap();
+ pending_events.push_back((events::Event::SpliceFailed {
+ channel_id: msg.channel_id,
+ counterparty_node_id,
+ user_channel_id: chan.context().get_user_id(),
+ abandoned_funding_txo: splice_funding_failed.funding_txo,
+ channel_type: splice_funding_failed.channel_type.clone(),
+ contributed_inputs: splice_funding_failed.contributed_inputs,
+ contributed_outputs: splice_funding_failed.contributed_outputs,
+ }, None));
+ }
+ Err(MsgHandleErrInternal::from_chan_no_close(error, msg.channel_id))
},
}
- Ok(())
},
hash_map::Entry::Vacant(_) => {
Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id))
@@ -14741,57 +14788,62 @@ where
}
fn handle_tx_add_input(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAddInput) {
- // Note that we never need to persist the updated ChannelManager for an inbound
- // tx_add_input message - interactive transaction construction does not need to
- // be persisted before any signatures are exchanged.
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
let res = self.internal_tx_add_input(counterparty_node_id, msg);
+ let persist = match &res {
+ Err(_) => NotifyOption::DoPersist,
+ Ok(persist) => *persist,
+ };
let _ = handle_error!(self, res, counterparty_node_id);
- NotifyOption::SkipPersistHandleEvents
+ persist
});
}
fn handle_tx_add_output(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAddOutput) {
- // Note that we never need to persist the updated ChannelManager for an inbound
- // tx_add_output message - interactive transaction construction does not need to
- // be persisted before any signatures are exchanged.
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
let res = self.internal_tx_add_output(counterparty_node_id, msg);
+ let persist = match &res {
+ Err(_) => NotifyOption::DoPersist,
+ Ok(persist) => *persist,
+ };
let _ = handle_error!(self, res, counterparty_node_id);
- NotifyOption::SkipPersistHandleEvents
+ persist
});
}
fn handle_tx_remove_input(&self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveInput) {
- // Note that we never need to persist the updated ChannelManager for an inbound
- // tx_remove_input message - interactive transaction construction does not need to
- // be persisted before any signatures are exchanged.
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
let res = self.internal_tx_remove_input(counterparty_node_id, msg);
+ let persist = match &res {
+ Err(_) => NotifyOption::DoPersist,
+ Ok(persist) => *persist,
+ };
let _ = handle_error!(self, res, counterparty_node_id);
- NotifyOption::SkipPersistHandleEvents
+ persist
});
}
fn handle_tx_remove_output(&self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveOutput) {
- // Note that we never need to persist the updated ChannelManager for an inbound
- // tx_remove_output message - interactive transaction construction does not need to
- // be persisted before any signatures are exchanged.
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
let res = self.internal_tx_remove_output(counterparty_node_id, msg);
+ let persist = match &res {
+ Err(_) => NotifyOption::DoPersist,
+ Ok(persist) => *persist,
+ };
let _ = handle_error!(self, res, counterparty_node_id);
- NotifyOption::SkipPersistHandleEvents
+ persist
});
}
fn handle_tx_complete(&self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete) {
- // Note that we never need to persist the updated ChannelManager for an inbound
- // tx_complete message - interactive transaction construction does not need to
- // be persisted before any signatures are exchanged.
let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || {
let res = self.internal_tx_complete(counterparty_node_id, msg);
+ let persist = match &res {
+ Err(_) => NotifyOption::DoPersist,
+ Ok(persist) => *persist,
+ };
let _ = handle_error!(self, res, counterparty_node_id);
- NotifyOption::SkipPersistHandleEvents
+ persist
});
}
diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs
index 28341e8..f7cc818 100644
--- a/lightning/src/ln/functional_test_utils.rs
+++ b/lightning/src/ln/functional_test_utils.rs
@@ -1054,6 +1054,29 @@ pub fn get_err_msg(node: &Node, recipient: &PublicKey) -> msgs::ErrorMessage {
}
}
+/// Get a warning message from the pending events queue.
+pub fn get_warning_msg(node: &Node, recipient: &PublicKey) -> msgs::WarningMessage {
+ let events = node.node.get_and_clear_pending_msg_events();
+ assert_eq!(events.len(), 1);
+ match events[0] {
+ MessageSendEvent::HandleError {
+ action: msgs::ErrorAction::DisconnectPeerWithWarning { ref msg },
+ ref node_id,
+ } => {
+ assert_eq!(node_id, recipient);
+ (*msg).clone()
+ },
+ MessageSendEvent::HandleError {
+ action: msgs::ErrorAction::SendWarningMessage { ref msg, .. },
+ ref node_id,
+ } => {
+ assert_eq!(node_id, recipient);
+ msg.clone()
+ },
+ _ => panic!("Unexpected event"),
+ }
+}
+
/// Get a specific event from the pending events queue.
#[macro_export]
macro_rules! get_event {
diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs
index cc90e10..db1d916 100644
--- a/lightning/src/ln/funding.rs
+++ b/lightning/src/ln/funding.rs
@@ -204,6 +204,11 @@ impl FundingTxInput {
FundingTxInput::new(prevtx, vout, Weight::ZERO, Script::is_p2pkh)
}
+ /// The outpoint of the UTXO being spent.
+ pub fn outpoint(&self) -> bitcoin::OutPoint {
+ self.utxo.outpoint
+ }
+
/// The sequence number to use in the [`TxIn`].
///
/// [`TxIn`]: bitcoin::TxIn
diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs
index d1cac89..b3c7356 100644
--- a/lightning/src/ln/interactivetxs.rs
+++ b/lightning/src/ln/interactivetxs.rs
@@ -345,6 +345,11 @@ impl ConstructedTransaction {
}
fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError {
+ let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs();
+ NegotiationError { reason, contributed_inputs, contributed_outputs }
+ }
+
+ fn into_contributed_inputs_and_outputs(self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) {
let contributed_inputs = self
.tx
.input
@@ -371,7 +376,7 @@ impl ConstructedTransaction {
.map(|(_, (txout, _))| txout)
.collect();
- NegotiationError { reason, contributed_inputs, contributed_outputs }
+ (contributed_inputs, contributed_outputs)
}
pub fn tx(&self) -> &Transaction {
@@ -852,6 +857,10 @@ impl InteractiveTxSigningSession {
pub(crate) fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError {
self.unsigned_tx.into_negotiation_error(reason)
}
+
+ pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) {
+ self.unsigned_tx.into_contributed_inputs_and_outputs()
+ }
}
impl_writeable_tlv_based!(InteractiveTxSigningSession, {
@@ -1885,6 +1894,7 @@ impl InteractiveTxInput {
pub(super) struct InteractiveTxConstructor {
state_machine: StateMachine,
+ is_initiator: bool,
initiator_first_message: Option<InteractiveTxMessageSend>,
channel_id: ChannelId,
inputs_to_contribute: Vec<(SerialId, InputOwned)>,
@@ -2047,6 +2057,7 @@ impl InteractiveTxConstructor {
let mut constructor = Self {
state_machine,
+ is_initiator,
initiator_first_message: None,
channel_id,
inputs_to_contribute,
@@ -2069,21 +2080,28 @@ impl InteractiveTxConstructor {
}
fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError {
- NegotiationError {
- reason,
- contributed_inputs: self
- .inputs_to_contribute
- .into_iter()
- .filter(|(_, input)| !input.is_shared())
- .map(|(_, input)| input.into_tx_in().previous_output)
- .collect(),
- contributed_outputs: self
- .outputs_to_contribute
- .into_iter()
- .filter(|(_, output)| !output.is_shared())
- .map(|(_, output)| output.into_tx_out())
- .collect(),
- }
+ let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs();
+ NegotiationError { reason, contributed_inputs, contributed_outputs }
+ }
+
+ pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) {
+ let contributed_inputs = self
+ .inputs_to_contribute
+ .into_iter()
+ .filter(|(_, input)| !input.is_shared())
+ .map(|(_, input)| input.into_tx_in().previous_output)
+ .collect();
+ let contributed_outputs = self
+ .outputs_to_contribute
+ .into_iter()
+ .filter(|(_, output)| !output.is_shared())
+ .map(|(_, output)| output.into_tx_out())
+ .collect();
+ (contributed_inputs, contributed_outputs)
+ }
+
+ pub fn is_initiator(&self) -> bool {
+ self.is_initiator
}
pub fn take_initiator_first_message(&mut self) -> Option<InteractiveTxMessageSend> {
diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs
index 4ce6976..4b24047 100644
--- a/lightning/src/ln/splicing_tests.rs
+++ b/lightning/src/ln/splicing_tests.rs
@@ -21,6 +21,7 @@ use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSe
use crate::ln::types::ChannelId;
use crate::util::errors::APIError;
use crate::util::ser::Writeable;
+use crate::util::test_channel_signer::SignerOp;
use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut};
@@ -68,6 +69,21 @@ fn negotiate_splice_tx<'a, 'b, 'c, 'd>(
initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
initiator_contribution: SpliceContribution,
) -> msgs::CommitmentSigned {
+ let new_funding_script =
+ complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone());
+ complete_interactive_funding_negotiation(
+ initiator,
+ acceptor,
+ channel_id,
+ initiator_contribution,
+ new_funding_script,
+ )
+}
+
+fn complete_splice_handshake<'a, 'b, 'c, 'd>(
+ initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId,
+ initiator_contribution: SpliceContribution,
+) -> ScriptBuf {
let node_id_initiator = initiator.node.get_our_node_id();
let node_id_acceptor = acceptor.node.get_our_node_id();
@@ -76,7 +92,7 @@ fn negotiate_splice_tx<'a, 'b, 'c, 'd>(
.splice_channel(
&channel_id,
&node_id_acceptor,
- initiator_contribution.clone(),
+ initiator_contribution,
FEERATE_FLOOR_SATS_PER_KW,
None,
)
@@ -98,13 +114,7 @@ fn negotiate_splice_tx<'a, 'b, 'c, 'd>(
)
.to_p2wsh();
- complete_interactive_funding_negotiation(
- initiator,
- acceptor,
- channel_id,
- initiator_contribution,
- new_funding_script,
- )
+ new_funding_script
}
fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>(
@@ -249,8 +259,16 @@ fn splice_channel<'a, 'b, 'c, 'd>(
let node_id_initiator = initiator.node.get_our_node_id();
let node_id_acceptor = acceptor.node.get_our_node_id();
- let initial_commit_sig_for_acceptor =
- negotiate_splice_tx(initiator, acceptor, channel_id, initiator_contribution);
+ let new_funding_script =
+ complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone());
+
+ let initial_commit_sig_for_acceptor = complete_interactive_funding_negotiation(
+ initiator,
+ acceptor,
+ channel_id,
+ initiator_contribution,
+ new_funding_script,
+ );
sign_interactive_funding_transaction(initiator, acceptor, initial_commit_sig_for_acceptor);
let splice_tx = {
@@ -1123,3 +1141,149 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) {
.chain_source
.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script);
}
+
+#[test]
+fn disconnect_on_unexpected_interactive_tx_message() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let config = test_default_anchors_channel_config();
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let initiator = &nodes[0];
+ let acceptor = &nodes[1];
+
+ let _node_id_initiator = initiator.node.get_our_node_id();
+ let node_id_acceptor = acceptor.node.get_our_node_id();
+
+ let initial_channel_capacity = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
+
+ let coinbase_tx = provide_anchor_reserves(&nodes);
+ let splice_in_amount = initial_channel_capacity / 2;
+ let contribution = SpliceContribution::SpliceIn {
+ value: Amount::from_sat(splice_in_amount),
+ inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
+ change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()),
+ };
+
+ // Complete interactive-tx construction, but fail by having the acceptor send a duplicate
+ // tx_complete instead of commitment_signed.
+ let _ = negotiate_splice_tx(initiator, acceptor, channel_id, contribution.clone());
+
+ let mut msg_events = acceptor.node.get_and_clear_pending_msg_events();
+ assert_eq!(msg_events.len(), 1);
+ assert!(matches!(msg_events.remove(0), MessageSendEvent::UpdateHTLCs { .. }));
+
+ let tx_complete = msgs::TxComplete { channel_id };
+ initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete);
+
+ let _warning = get_warning_msg(initiator, &node_id_acceptor);
+}
+
+#[test]
+fn fail_splice_on_interactive_tx_error() {
+ let chanmon_cfgs = create_chanmon_cfgs(2);
+ let node_cfgs = create_node_cfgs(2, &chanmon_cfgs);
+ let config = test_default_anchors_channel_config();
+ let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]);
+ let nodes = create_network(2, &node_cfgs, &node_chanmgrs);
+
+ let initiator = &nodes[0];
+ let acceptor = &nodes[1];
+
+ let node_id_initiator = initiator.node.get_our_node_id();
+ let node_id_acceptor = acceptor.node.get_our_node_id();
+
+ let initial_channel_capacity = 100_000;
+ let (_, _, channel_id, _) =
+ create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0);
+
+ let coinbase_tx = provide_anchor_reserves(&nodes);
+ let splice_in_amount = initial_channel_capacity / 2;
+ let contribution = SpliceContribution::SpliceIn {
+ value: Amount::from_sat(splice_in_amount),
+ inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()],
+ change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()),
+ };
+
+ // Fail during interactive-tx construction by having the acceptor echo back tx_add_input instead
+ // of sending tx_complete. The failure occurs because the serial id will have the wrong parity.
+ let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone());
+
+ let tx_add_input =
+ get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
+ acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input);
+
+ let _tx_complete =
+ get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator);
+ initiator.node.handle_tx_add_input(node_id_acceptor, &tx_add_input);
+
+ let event = get_event!(initiator, Event::SpliceFailed);
+ match event {
+ Event::SpliceFailed { contributed_inputs, .. } => {
+ assert_eq!(contributed_inputs.len(), 1);
+ assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint());
+ },
+ _ => panic!("Expected Event::SpliceFailed"),
+ }
+
+ let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor);
+ acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort);
+
+ let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator);
+ initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort);
+
+ // Fail signing the commitment transaction, which prevents the initiator from sending
+ // tx_complete.
+ initiator.disable_channel_signer_op(
+ &node_id_acceptor,
+ &channel_id,
+ SignerOp::SignCounterpartyCommitment,
+ );
+ let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone());
+
+ let tx_add_input =
+ get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
+ acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input);
+
+ let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator);
+ initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete);
+
+ let tx_add_input =
+ get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor);
+ acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input);
+
+ let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator);
+ initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete);
+
+ let tx_add_output =
+ get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor);
+ acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output);
+
+ let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator);
+ initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete);
+
+ let tx_add_output =
+ get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor);
+ acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output);
+
+ let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator);
+ initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete);
+
+ let event = get_event!(initiator, Event::SpliceFailed);
+ match event {
+ Event::SpliceFailed { contributed_inputs, .. } => {
+ assert_eq!(contributed_inputs.len(), 1);
+ assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint());
+ },
+ _ => panic!("Expected Event::SpliceFailed"),
+ }
+
+ let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor);
+ acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort);
+
+ let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator);
+ initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort);
+}
Why this scored 32/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.