-
Notifications
You must be signed in to change notification settings - Fork 444
Allow cancellation of pending splice funding negotiations #4490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4741,7 +4741,9 @@ impl< | |
| /// [`Event::DiscardFunding`] is seen. | ||
| /// | ||
| /// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`] | ||
| /// will be generated and [`ChannelManager::funding_transaction_signed`] should be called. | ||
| /// may be generated. To proceed, call [`ChannelManager::funding_transaction_signed`]. To cancel | ||
| /// the pending splice negotiation instead, call [`ChannelManager::cancel_splice`] before | ||
| /// providing the funding signatures. | ||
| /// | ||
| /// If any failures occur while negotiating the funding transaction, an [`Event::SpliceFailed`] | ||
| /// will be emitted. Any contributed inputs no longer used will be included here and thus can | ||
|
|
@@ -4887,96 +4889,108 @@ impl< | |
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| pub(crate) fn abandon_splice( | ||
| &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, | ||
| ) -> Result<(), APIError> { | ||
| let mut res = Ok(()); | ||
| PersistenceNotifierGuard::optionally_notify(self, || { | ||
| let result = self.internal_abandon_splice(channel_id, counterparty_node_id); | ||
| res = result; | ||
| match res { | ||
| Ok(_) => NotifyOption::SkipPersistHandleEvents, | ||
| Err(_) => NotifyOption::SkipPersistNoEvents, | ||
| } | ||
| }); | ||
| res | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| fn internal_abandon_splice( | ||
| /// Cancels a pending splice negotiation for which a local contribution was made and queues a | ||
| /// `tx_abort` for the counterparty. | ||
| /// | ||
| /// This is primarily useful after receiving an [`Event::FundingTransactionReadyForSigning`] for | ||
| /// a splice if you no longer wish to proceed. The pending splice must still be pending | ||
| /// negotiation, which for the final signing stage means | ||
| /// [`ChannelManager::funding_transaction_signed`] must not have been called yet. | ||
| /// | ||
| /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect | ||
| /// `counterparty_node_id` is provided. | ||
| /// | ||
| /// Returns [`APIMisuseError`] when the channel is not funded, has no pending splice to cancel, | ||
| /// the pending splice has no local contribution to reclaim, or the pending splice can no longer | ||
| /// be canceled. | ||
| /// | ||
| /// [`Event::FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning | ||
| /// [`ChannelUnavailable`]: APIError::ChannelUnavailable | ||
| /// [`APIMisuseError`]: APIError::APIMisuseError | ||
| pub fn cancel_splice( | ||
| &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, | ||
| ) -> Result<(), APIError> { | ||
| let per_peer_state = self.per_peer_state.read().unwrap(); | ||
|
|
||
| let peer_state_mutex = match per_peer_state | ||
| .get(counterparty_node_id) | ||
| .ok_or_else(|| APIError::no_such_peer(counterparty_node_id)) | ||
| { | ||
| Ok(p) => p, | ||
| Err(e) => return Err(e), | ||
| }; | ||
|
|
||
| let mut peer_state_lock = peer_state_mutex.lock().unwrap(); | ||
| let peer_state = &mut *peer_state_lock; | ||
| let mut result = Ok(()); | ||
| PersistenceNotifierGuard::manually_notify(self, || { | ||
| let per_peer_state = self.per_peer_state.read().unwrap(); | ||
| let peer_state_mutex = match per_peer_state | ||
| .get(counterparty_node_id) | ||
| .ok_or_else(|| APIError::no_such_peer(counterparty_node_id)) | ||
| { | ||
| Ok(p) => p, | ||
| Err(e) => { | ||
| result = Err(e); | ||
| return; | ||
| }, | ||
| }; | ||
| let mut peer_state_lock = peer_state_mutex.lock().unwrap(); | ||
| let peer_state = &mut *peer_state_lock; | ||
|
|
||
| // Look for the channel | ||
| match peer_state.channel_by_id.entry(*channel_id) { | ||
| hash_map::Entry::Occupied(mut chan_phase_entry) => { | ||
| if !chan_phase_entry.get().context().is_connected() { | ||
| // TODO: We should probably support this, but right now `splice_channel` refuses when | ||
| // the peer is disconnected, so we just check it here. | ||
| return Err(APIError::ChannelUnavailable { | ||
| err: "Cannot abandon splice while peer is disconnected".to_owned(), | ||
| }); | ||
| } | ||
| match peer_state.channel_by_id.entry(*channel_id) { | ||
| hash_map::Entry::Occupied(mut chan_entry) => { | ||
| if let Some(channel) = chan_entry.get_mut().as_funded_mut() { | ||
| let InteractiveTxMsgError { err, splice_funding_failed, exited_quiescence } = | ||
| match channel.cancel_splice() { | ||
| Ok(v) => v, | ||
| Err(e) => { | ||
| result = Err(e); | ||
| return; | ||
| }, | ||
| }; | ||
|
|
||
| if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { | ||
| let (tx_abort, splice_funding_failed) = chan.abandon_splice()?; | ||
| let splice_funding_failed = splice_funding_failed | ||
| .expect("Only splices with local contributions can be canceled"); | ||
|
Comment on lines
+4941
to
+4942
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This The |
||
| { | ||
| let pending_events = &mut self.pending_events.lock().unwrap(); | ||
| pending_events.push_back(( | ||
| events::Event::SpliceFailed { | ||
| channel_id: *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(), | ||
| }, | ||
| None, | ||
| )); | ||
| pending_events.push_back(( | ||
| events::Event::DiscardFunding { | ||
| channel_id: *channel_id, | ||
| funding_info: FundingInfo::Contribution { | ||
| inputs: splice_funding_failed.contributed_inputs, | ||
| outputs: splice_funding_failed.contributed_outputs, | ||
| }, | ||
| }, | ||
| None, | ||
| )); | ||
| } | ||
|
|
||
| peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort { | ||
| node_id: *counterparty_node_id, | ||
| msg: tx_abort, | ||
| }); | ||
| mem::drop(peer_state_lock); | ||
| mem::drop(per_peer_state); | ||
|
|
||
| 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: *channel_id, | ||
| counterparty_node_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, | ||
| }, | ||
| None, | ||
| )); | ||
| pending_events.push_back(( | ||
| events::Event::DiscardFunding { | ||
| channel_id: *channel_id, | ||
| funding_info: FundingInfo::Contribution { | ||
| inputs: splice_funding_failed.contributed_inputs, | ||
| outputs: splice_funding_failed.contributed_outputs, | ||
| }, | ||
| }, | ||
| None, | ||
| )); | ||
| self.needs_persist_flag.store(true, Ordering::Release); | ||
| self.event_persist_notifier.notify(); | ||
| let err: Result<(), _> = | ||
| Err(MsgHandleErrInternal::from_chan_no_close(err, *channel_id) | ||
| .with_exited_quiescence(exited_quiescence)); | ||
| let _ = self.handle_error(err, *counterparty_node_id); | ||
| } else { | ||
| result = Err(APIError::ChannelUnavailable { | ||
| err: format!( | ||
| "Channel with id {} is not funded, cannot cancel splice", | ||
| channel_id | ||
| ), | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| Ok(()) | ||
| } else { | ||
| Err(APIError::ChannelUnavailable { | ||
| err: format!( | ||
| "Channel with id {} is not funded, cannot abandon splice", | ||
| channel_id | ||
| ), | ||
| }) | ||
| } | ||
| }, | ||
| hash_map::Entry::Vacant(_) => { | ||
| Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)) | ||
| }, | ||
| } | ||
| }, | ||
| hash_map::Entry::Vacant(_) => { | ||
| result = | ||
| Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)); | ||
| return; | ||
| }, | ||
| } | ||
| }); | ||
| result | ||
| } | ||
|
|
||
| fn forward_needs_intercept_to_known_chan( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: the contribution check here (
has_local_contribution) differs in how it computes "has contributions" compared to theAwaitingAck/ConstructingTransactionbranches.has_local_contribution()uses the signing session'slocal_inputs_count/local_outputs_count(which filter byis_localon the constructed transaction metadata), while the other branches usecontributed_inputs()/contributed_outputs()from the context/constructor (which useour_funding_inputs/our_funding_outputs).These should be semantically equivalent since both exclude the shared input and reflect local contributions, but the different computation paths mean a divergence in one path wouldn't be caught by the other. Consider adding a comment noting the equivalence, or unifying the approach.