Skip to content

Commit 00254f4

Browse files
committed
Allow cancellation of pending splice funding negotiations
A user may wish to cancel an in-flight funding negotiation for whatever reason (e.g., mempool feerates have gone down, inability to sign, etc.), so we should make it possible for them to do so. Note that this can only be done for splice funding negotiations for which the user has made a contribution to.
1 parent 76977af commit 00254f4

File tree

5 files changed

+424
-108
lines changed

5 files changed

+424
-108
lines changed

lightning/src/events/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,7 +1833,7 @@ pub enum Event {
18331833
invoice_request: InvoiceRequest,
18341834
},
18351835
/// Indicates that a channel funding transaction constructed interactively is ready to be
1836-
/// signed. This event will only be triggered if at least one input was contributed.
1836+
/// signed. This event will only be triggered if a contribution was made to the transaction.
18371837
///
18381838
/// The transaction contains all inputs and outputs provided by both parties including the
18391839
/// channel's funding output and a change output if applicable.
@@ -1844,8 +1844,9 @@ pub enum Event {
18441844
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
18451845
/// hence possible loss of funds.
18461846
///
1847-
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
1848-
/// funding transaction.
1847+
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
1848+
/// signed funding transaction. For splices where you contributed inputs or outputs, call
1849+
/// [`ChannelManager::cancel_splice`] instead if you no longer wish to proceed.
18491850
///
18501851
/// Generated in [`ChannelManager`] message handling.
18511852
///
@@ -1854,6 +1855,7 @@ pub enum Event {
18541855
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
18551856
///
18561857
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
1858+
/// [`ChannelManager::cancel_splice`]: crate::ln::channelmanager::ChannelManager::cancel_splice
18571859
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
18581860
FundingTransactionReadyForSigning {
18591861
/// The `channel_id` of the channel which you'll need to pass back into

lightning/src/ln/channel.rs

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12255,30 +12255,77 @@ where
1225512255
}
1225612256
}
1225712257

12258-
#[cfg(test)]
12259-
pub fn abandon_splice(
12260-
&mut self,
12261-
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
12262-
if self.should_reset_pending_splice_state(false) {
12263-
let tx_abort =
12264-
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
12265-
let splice_funding_failed = self.reset_pending_splice_state();
12266-
Ok((tx_abort, splice_funding_failed))
12267-
} else if self.has_pending_splice_awaiting_signatures() {
12268-
Err(APIError::APIMisuseError {
12258+
pub fn cancel_splice(&mut self) -> Result<InteractiveTxMsgError, APIError> {
12259+
let funding_negotiation = self
12260+
.pending_splice
12261+
.as_ref()
12262+
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
12263+
let Some(funding_negotiation) = funding_negotiation else {
12264+
return Err(APIError::APIMisuseError {
1226912265
err: format!(
12270-
"Channel {} splice cannot be abandoned; already awaiting signatures",
12271-
self.context.channel_id(),
12266+
"Channel {} does not have a pending splice negotiation",
12267+
self.context.channel_id()
1227212268
),
12273-
})
12274-
} else {
12275-
Err(APIError::APIMisuseError {
12269+
});
12270+
};
12271+
12272+
let made_contribution = match funding_negotiation {
12273+
FundingNegotiation::AwaitingAck { context, .. } => {
12274+
context.contributed_inputs().next().is_some()
12275+
|| context.contributed_outputs().next().is_some()
12276+
},
12277+
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
12278+
interactive_tx_constructor.contributed_inputs().next().is_some()
12279+
|| interactive_tx_constructor.contributed_outputs().next().is_some()
12280+
},
12281+
FundingNegotiation::AwaitingSignatures { .. } => self
12282+
.context
12283+
.interactive_tx_signing_session
12284+
.as_ref()
12285+
.expect("We have a pending splice awaiting signatures")
12286+
.has_local_contribution(),
12287+
};
12288+
if !made_contribution {
12289+
return Err(APIError::APIMisuseError {
1227612290
err: format!(
12277-
"Channel {} splice cannot be abandoned; no pending splice",
12278-
self.context.channel_id(),
12291+
"Channel {} has a pending splice negotiation with no contribution made",
12292+
self.context.channel_id()
1227912293
),
12280-
})
12294+
});
12295+
}
12296+
12297+
// We typically don't reset the pending funding negotiation when we're in
12298+
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
12299+
// re-establishment, so we still need to handle this case separately if the user wishes to
12300+
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
12301+
// guarantee to never have sent any signatures to the counterparty, or have processed any
12302+
// signatures from them.
12303+
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
12304+
let already_signed = self
12305+
.context
12306+
.interactive_tx_signing_session
12307+
.as_ref()
12308+
.expect("We have a pending splice awaiting signatures")
12309+
.holder_tx_signatures()
12310+
.is_some();
12311+
if already_signed {
12312+
return Err(APIError::APIMisuseError {
12313+
err: format!(
12314+
"Channel {} has pending splice negotiation that was already signed",
12315+
self.context.channel_id(),
12316+
),
12317+
});
12318+
}
1228112319
}
12320+
12321+
debug_assert!(self.context.channel_state.is_quiescent());
12322+
let splice_funding_failed = self.reset_pending_splice_state();
12323+
debug_assert!(splice_funding_failed.is_some());
12324+
Ok(InteractiveTxMsgError {
12325+
err: ChannelError::Abort(AbortReason::ManualIntervention),
12326+
splice_funding_failed,
12327+
exited_quiescence: true,
12328+
})
1228212329
}
1228312330

1228412331
/// Checks during handling splice_init

lightning/src/ln/channelmanager.rs

Lines changed: 98 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4741,7 +4741,9 @@ impl<
47414741
/// [`Event::DiscardFunding`] is seen.
47424742
///
47434743
/// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`]
4744-
/// will be generated and [`ChannelManager::funding_transaction_signed`] should be called.
4744+
/// may be generated. To proceed, call [`ChannelManager::funding_transaction_signed`]. To cancel
4745+
/// the pending splice negotiation instead, call [`ChannelManager::cancel_splice`] before
4746+
/// providing the funding signatures.
47454747
///
47464748
/// If any failures occur while negotiating the funding transaction, an [`Event::SpliceFailed`]
47474749
/// will be emitted. Any contributed inputs no longer used will be included here and thus can
@@ -4887,96 +4889,108 @@ impl<
48874889
}
48884890
}
48894891

4890-
#[cfg(test)]
4891-
pub(crate) fn abandon_splice(
4892-
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
4893-
) -> Result<(), APIError> {
4894-
let mut res = Ok(());
4895-
PersistenceNotifierGuard::optionally_notify(self, || {
4896-
let result = self.internal_abandon_splice(channel_id, counterparty_node_id);
4897-
res = result;
4898-
match res {
4899-
Ok(_) => NotifyOption::SkipPersistHandleEvents,
4900-
Err(_) => NotifyOption::SkipPersistNoEvents,
4901-
}
4902-
});
4903-
res
4904-
}
4905-
4906-
#[cfg(test)]
4907-
fn internal_abandon_splice(
4892+
/// Cancels a pending splice negotiation for which a local contribution was made and queues a
4893+
/// `tx_abort` for the counterparty.
4894+
///
4895+
/// This is primarily useful after receiving an [`Event::FundingTransactionReadyForSigning`] for
4896+
/// a splice if you no longer wish to proceed. The pending splice must still be pending
4897+
/// negotiation, which for the final signing stage means
4898+
/// [`ChannelManager::funding_transaction_signed`] must not have been called yet.
4899+
///
4900+
/// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect
4901+
/// `counterparty_node_id` is provided.
4902+
///
4903+
/// Returns [`APIMisuseError`] when the channel is not funded, has no pending splice to cancel,
4904+
/// the pending splice has no local contribution to reclaim, or the pending splice can no longer
4905+
/// be canceled.
4906+
///
4907+
/// [`Event::FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning
4908+
/// [`ChannelUnavailable`]: APIError::ChannelUnavailable
4909+
/// [`APIMisuseError`]: APIError::APIMisuseError
4910+
pub fn cancel_splice(
49084911
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
49094912
) -> Result<(), APIError> {
4910-
let per_peer_state = self.per_peer_state.read().unwrap();
4911-
4912-
let peer_state_mutex = match per_peer_state
4913-
.get(counterparty_node_id)
4914-
.ok_or_else(|| APIError::no_such_peer(counterparty_node_id))
4915-
{
4916-
Ok(p) => p,
4917-
Err(e) => return Err(e),
4918-
};
4919-
4920-
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
4921-
let peer_state = &mut *peer_state_lock;
4913+
let mut result = Ok(());
4914+
PersistenceNotifierGuard::manually_notify(self, || {
4915+
let per_peer_state = self.per_peer_state.read().unwrap();
4916+
let peer_state_mutex = match per_peer_state
4917+
.get(counterparty_node_id)
4918+
.ok_or_else(|| APIError::no_such_peer(counterparty_node_id))
4919+
{
4920+
Ok(p) => p,
4921+
Err(e) => {
4922+
result = Err(e);
4923+
return;
4924+
},
4925+
};
4926+
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
4927+
let peer_state = &mut *peer_state_lock;
49224928

4923-
// Look for the channel
4924-
match peer_state.channel_by_id.entry(*channel_id) {
4925-
hash_map::Entry::Occupied(mut chan_phase_entry) => {
4926-
if !chan_phase_entry.get().context().is_connected() {
4927-
// TODO: We should probably support this, but right now `splice_channel` refuses when
4928-
// the peer is disconnected, so we just check it here.
4929-
return Err(APIError::ChannelUnavailable {
4930-
err: "Cannot abandon splice while peer is disconnected".to_owned(),
4931-
});
4932-
}
4929+
match peer_state.channel_by_id.entry(*channel_id) {
4930+
hash_map::Entry::Occupied(mut chan_entry) => {
4931+
if let Some(channel) = chan_entry.get_mut().as_funded_mut() {
4932+
let InteractiveTxMsgError { err, splice_funding_failed, exited_quiescence } =
4933+
match channel.cancel_splice() {
4934+
Ok(v) => v,
4935+
Err(e) => {
4936+
result = Err(e);
4937+
return;
4938+
},
4939+
};
49334940

4934-
if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() {
4935-
let (tx_abort, splice_funding_failed) = chan.abandon_splice()?;
4941+
let splice_funding_failed = splice_funding_failed
4942+
.expect("Only splices with local contributions can be canceled");
4943+
{
4944+
let pending_events = &mut self.pending_events.lock().unwrap();
4945+
pending_events.push_back((
4946+
events::Event::SpliceFailed {
4947+
channel_id: *channel_id,
4948+
counterparty_node_id: *counterparty_node_id,
4949+
user_channel_id: channel.context().get_user_id(),
4950+
abandoned_funding_txo: splice_funding_failed.funding_txo,
4951+
channel_type: splice_funding_failed.channel_type.clone(),
4952+
},
4953+
None,
4954+
));
4955+
pending_events.push_back((
4956+
events::Event::DiscardFunding {
4957+
channel_id: *channel_id,
4958+
funding_info: FundingInfo::Contribution {
4959+
inputs: splice_funding_failed.contributed_inputs,
4960+
outputs: splice_funding_failed.contributed_outputs,
4961+
},
4962+
},
4963+
None,
4964+
));
4965+
}
49364966

4937-
peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort {
4938-
node_id: *counterparty_node_id,
4939-
msg: tx_abort,
4940-
});
4967+
mem::drop(peer_state_lock);
4968+
mem::drop(per_peer_state);
49414969

4942-
if let Some(splice_funding_failed) = splice_funding_failed {
4943-
let pending_events = &mut self.pending_events.lock().unwrap();
4944-
pending_events.push_back((
4945-
events::Event::SpliceFailed {
4946-
channel_id: *channel_id,
4947-
counterparty_node_id: *counterparty_node_id,
4948-
user_channel_id: chan.context.get_user_id(),
4949-
abandoned_funding_txo: splice_funding_failed.funding_txo,
4950-
channel_type: splice_funding_failed.channel_type,
4951-
},
4952-
None,
4953-
));
4954-
pending_events.push_back((
4955-
events::Event::DiscardFunding {
4956-
channel_id: *channel_id,
4957-
funding_info: FundingInfo::Contribution {
4958-
inputs: splice_funding_failed.contributed_inputs,
4959-
outputs: splice_funding_failed.contributed_outputs,
4960-
},
4961-
},
4962-
None,
4963-
));
4970+
self.needs_persist_flag.store(true, Ordering::Release);
4971+
self.event_persist_notifier.notify();
4972+
let err: Result<(), _> =
4973+
Err(MsgHandleErrInternal::from_chan_no_close(err, *channel_id)
4974+
.with_exited_quiescence(exited_quiescence));
4975+
let _ = self.handle_error(err, *counterparty_node_id);
4976+
} else {
4977+
result = Err(APIError::ChannelUnavailable {
4978+
err: format!(
4979+
"Channel with id {} is not funded, cannot cancel splice",
4980+
channel_id
4981+
),
4982+
});
4983+
return;
49644984
}
4965-
4966-
Ok(())
4967-
} else {
4968-
Err(APIError::ChannelUnavailable {
4969-
err: format!(
4970-
"Channel with id {} is not funded, cannot abandon splice",
4971-
channel_id
4972-
),
4973-
})
4974-
}
4975-
},
4976-
hash_map::Entry::Vacant(_) => {
4977-
Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id))
4978-
},
4979-
}
4985+
},
4986+
hash_map::Entry::Vacant(_) => {
4987+
result =
4988+
Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id));
4989+
return;
4990+
},
4991+
}
4992+
});
4993+
result
49804994
}
49814995

49824996
fn forward_needs_intercept_to_known_chan(

lightning/src/ln/interactivetxs.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ pub(crate) enum AbortReason {
143143
NegotiationInProgress,
144144
/// The initiator's feerate exceeds our maximum.
145145
FeeRateTooHigh,
146+
/// The user manually intervened to abort the funding negotiation.
147+
ManualIntervention,
146148
/// Internal error
147149
InternalError(&'static str),
148150
}
@@ -209,6 +211,7 @@ impl Display for AbortReason {
209211
AbortReason::FeeRateTooHigh => {
210212
f.write_str("The initiator's feerate exceeds our maximum")
211213
},
214+
AbortReason::ManualIntervention => f.write_str("Manually aborted funding negotiation"),
212215
AbortReason::InternalError(text) => {
213216
f.write_fmt(format_args!("Internal error: {}", text))
214217
},

0 commit comments

Comments
 (0)