Skip to content

Commit 6bad3fd

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 86dc928 commit 6bad3fd

5 files changed

Lines changed: 460 additions & 107 deletions

File tree

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_funding_contributed`] 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_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
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: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12659,30 +12659,76 @@ where
1265912659
}
1266012660
}
1266112661

12662-
#[cfg(test)]
12663-
pub fn abandon_splice(
12664-
&mut self,
12665-
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
12666-
if self.should_reset_pending_splice_state(false) {
12667-
let tx_abort =
12668-
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
12669-
let splice_funding_failed = self.reset_pending_splice_state();
12670-
Ok((tx_abort, splice_funding_failed))
12671-
} else if self.has_pending_splice_awaiting_signatures() {
12672-
Err(APIError::APIMisuseError {
12662+
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
12663+
let funding_negotiation = self
12664+
.pending_splice
12665+
.as_ref()
12666+
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
12667+
let Some(funding_negotiation) = funding_negotiation else {
12668+
return Err(APIError::APIMisuseError {
1267312669
err: format!(
12674-
"Channel {} splice cannot be abandoned; already awaiting signatures",
12675-
self.context.channel_id(),
12670+
"Channel {} does not have a pending splice negotiation",
12671+
self.context.channel_id()
1267612672
),
12677-
})
12678-
} else {
12679-
Err(APIError::APIMisuseError {
12673+
});
12674+
};
12675+
12676+
let made_contribution = match funding_negotiation {
12677+
FundingNegotiation::AwaitingAck { context, .. } => {
12678+
context.contributed_inputs().next().is_some()
12679+
|| context.contributed_outputs().next().is_some()
12680+
},
12681+
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
12682+
interactive_tx_constructor.contributed_inputs().next().is_some()
12683+
|| interactive_tx_constructor.contributed_outputs().next().is_some()
12684+
},
12685+
FundingNegotiation::AwaitingSignatures { .. } => self
12686+
.context
12687+
.interactive_tx_signing_session
12688+
.as_ref()
12689+
.expect("We have a pending splice awaiting signatures")
12690+
.has_local_contribution(),
12691+
};
12692+
if !made_contribution {
12693+
return Err(APIError::APIMisuseError {
1268012694
err: format!(
12681-
"Channel {} splice cannot be abandoned; no pending splice",
12682-
self.context.channel_id(),
12695+
"Channel {} has a pending splice negotiation with no contribution made",
12696+
self.context.channel_id()
1268312697
),
12684-
})
12698+
});
1268512699
}
12700+
12701+
// We typically don't reset the pending funding negotiation when we're in
12702+
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
12703+
// re-establishment, so we still need to handle this case separately if the user wishes to
12704+
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
12705+
// guarantee to never have sent any signatures to the counterparty, or have processed any
12706+
// signatures from them.
12707+
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
12708+
let already_signed = self
12709+
.context
12710+
.interactive_tx_signing_session
12711+
.as_ref()
12712+
.expect("We have a pending splice awaiting signatures")
12713+
.has_holder_tx_signatures();
12714+
if already_signed {
12715+
return Err(APIError::APIMisuseError {
12716+
err: format!(
12717+
"Channel {} has pending splice negotiation that was already signed",
12718+
self.context.channel_id(),
12719+
),
12720+
});
12721+
}
12722+
}
12723+
12724+
debug_assert!(self.context.channel_state.is_quiescent());
12725+
let splice_funding_failed = self.reset_pending_splice_state();
12726+
debug_assert!(splice_funding_failed.is_some());
12727+
Ok(InteractiveTxMsgError {
12728+
err: ChannelError::Abort(AbortReason::ManualIntervention),
12729+
splice_funding_failed,
12730+
exited_quiescence: true,
12731+
})
1268612732
}
1268712733

1268812734
/// Checks during handling splice_init

lightning/src/ln/channelmanager.rs

Lines changed: 90 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4841,96 +4841,103 @@ impl<
48414841
}
48424842
}
48434843

4844-
#[cfg(test)]
4845-
pub(crate) fn abandon_splice(
4846-
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
4847-
) -> Result<(), APIError> {
4848-
let mut res = Ok(());
4849-
PersistenceNotifierGuard::optionally_notify(self, || {
4850-
let result = self.internal_abandon_splice(channel_id, counterparty_node_id);
4851-
res = result;
4852-
match res {
4853-
Ok(_) => NotifyOption::SkipPersistHandleEvents,
4854-
Err(_) => NotifyOption::SkipPersistNoEvents,
4855-
}
4856-
});
4857-
res
4858-
}
4859-
4860-
#[cfg(test)]
4861-
fn internal_abandon_splice(
4844+
/// Cancels an in-flight [`FundingContribution`].
4845+
///
4846+
/// This is primarily useful after receiving an [`Event::FundingTransactionReadyForSigning`] for
4847+
/// a [`FundingContribution`] you no longer wish to proceed with. Canceling is only allowed up
4848+
/// until [`ChannelManager::funding_transaction_signed`] is called for the corresponding
4849+
/// [`FundingContribution`].
4850+
///
4851+
/// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect
4852+
/// `counterparty_node_id` is provided, or [`APIMisuseError`] otherwise with the error details.
4853+
///
4854+
/// [`Event::FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning
4855+
/// [`ChannelUnavailable`]: APIError::ChannelUnavailable
4856+
/// [`APIMisuseError`]: APIError::APIMisuseError
4857+
pub fn cancel_funding_contributed(
48624858
&self, channel_id: &ChannelId, counterparty_node_id: &PublicKey,
48634859
) -> Result<(), APIError> {
4864-
let per_peer_state = self.per_peer_state.read().unwrap();
4865-
4866-
let peer_state_mutex = match per_peer_state
4867-
.get(counterparty_node_id)
4868-
.ok_or_else(|| APIError::no_such_peer(counterparty_node_id))
4869-
{
4870-
Ok(p) => p,
4871-
Err(e) => return Err(e),
4872-
};
4873-
4874-
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
4875-
let peer_state = &mut *peer_state_lock;
4860+
let mut result = Ok(());
4861+
PersistenceNotifierGuard::manually_notify(self, || {
4862+
let per_peer_state = self.per_peer_state.read().unwrap();
4863+
let peer_state_mutex = match per_peer_state
4864+
.get(counterparty_node_id)
4865+
.ok_or_else(|| APIError::no_such_peer(counterparty_node_id))
4866+
{
4867+
Ok(p) => p,
4868+
Err(e) => {
4869+
result = Err(e);
4870+
return;
4871+
},
4872+
};
4873+
let mut peer_state_lock = peer_state_mutex.lock().unwrap();
4874+
let peer_state = &mut *peer_state_lock;
48764875

4877-
// Look for the channel
4878-
match peer_state.channel_by_id.entry(*channel_id) {
4879-
hash_map::Entry::Occupied(mut chan_phase_entry) => {
4880-
if !chan_phase_entry.get().context().is_connected() {
4881-
// TODO: We should probably support this, but right now `splice_channel` refuses when
4882-
// the peer is disconnected, so we just check it here.
4883-
return Err(APIError::ChannelUnavailable {
4884-
err: "Cannot abandon splice while peer is disconnected".to_owned(),
4885-
});
4886-
}
4876+
match peer_state.channel_by_id.entry(*channel_id) {
4877+
hash_map::Entry::Occupied(mut chan_entry) => {
4878+
if let Some(channel) = chan_entry.get_mut().as_funded_mut() {
4879+
let InteractiveTxMsgError { err, splice_funding_failed, exited_quiescence } =
4880+
match channel.cancel_funding_contributed() {
4881+
Ok(v) => v,
4882+
Err(e) => {
4883+
result = Err(e);
4884+
return;
4885+
},
4886+
};
48874887

4888-
if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() {
4889-
let (tx_abort, splice_funding_failed) = chan.abandon_splice()?;
4888+
let splice_funding_failed = splice_funding_failed
4889+
.expect("Only splices with local contributions can be canceled");
4890+
{
4891+
let pending_events = &mut self.pending_events.lock().unwrap();
4892+
pending_events.push_back((
4893+
events::Event::SpliceFailed {
4894+
channel_id: *channel_id,
4895+
counterparty_node_id: *counterparty_node_id,
4896+
user_channel_id: channel.context().get_user_id(),
4897+
abandoned_funding_txo: splice_funding_failed.funding_txo,
4898+
channel_type: splice_funding_failed.channel_type.clone(),
4899+
},
4900+
None,
4901+
));
4902+
pending_events.push_back((
4903+
events::Event::DiscardFunding {
4904+
channel_id: *channel_id,
4905+
funding_info: FundingInfo::Contribution {
4906+
inputs: splice_funding_failed.contributed_inputs,
4907+
outputs: splice_funding_failed.contributed_outputs,
4908+
},
4909+
},
4910+
None,
4911+
));
4912+
}
48904913

4891-
peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort {
4892-
node_id: *counterparty_node_id,
4893-
msg: tx_abort,
4894-
});
4914+
mem::drop(peer_state_lock);
4915+
mem::drop(per_peer_state);
48954916

4896-
if let Some(splice_funding_failed) = splice_funding_failed {
4897-
let pending_events = &mut self.pending_events.lock().unwrap();
4898-
pending_events.push_back((
4899-
events::Event::SpliceFailed {
4900-
channel_id: *channel_id,
4901-
counterparty_node_id: *counterparty_node_id,
4902-
user_channel_id: chan.context.get_user_id(),
4903-
abandoned_funding_txo: splice_funding_failed.funding_txo,
4904-
channel_type: splice_funding_failed.channel_type,
4905-
},
4906-
None,
4907-
));
4908-
pending_events.push_back((
4909-
events::Event::DiscardFunding {
4910-
channel_id: *channel_id,
4911-
funding_info: FundingInfo::Contribution {
4912-
inputs: splice_funding_failed.contributed_inputs,
4913-
outputs: splice_funding_failed.contributed_outputs,
4914-
},
4915-
},
4916-
None,
4917-
));
4917+
self.needs_persist_flag.store(true, Ordering::Release);
4918+
self.event_persist_notifier.notify();
4919+
let err: Result<(), _> =
4920+
Err(MsgHandleErrInternal::from_chan_no_close(err, *channel_id)
4921+
.with_exited_quiescence(exited_quiescence));
4922+
let _ = self.handle_error(err, *counterparty_node_id);
4923+
} else {
4924+
result = Err(APIError::ChannelUnavailable {
4925+
err: format!(
4926+
"Channel with id {} is not funded, cannot cancel splice",
4927+
channel_id
4928+
),
4929+
});
4930+
return;
49184931
}
4919-
4920-
Ok(())
4921-
} else {
4922-
Err(APIError::ChannelUnavailable {
4923-
err: format!(
4924-
"Channel with id {} is not funded, cannot abandon splice",
4925-
channel_id
4926-
),
4927-
})
4928-
}
4929-
},
4930-
hash_map::Entry::Vacant(_) => {
4931-
Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id))
4932-
},
4933-
}
4932+
},
4933+
hash_map::Entry::Vacant(_) => {
4934+
result =
4935+
Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id));
4936+
return;
4937+
},
4938+
}
4939+
});
4940+
result
49344941
}
49354942

49364943
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)