Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions lightning/src/events/mod.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -137,8 +137,10 @@ pub enum NegotiationFailureReason {
/// [`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 negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`].
///
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
LocallyCanceled,
/// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`]
/// for the closure reason.
ChannelClosing,
Expand DownExpand Up@@ -171,7 +173,7 @@ impl NegotiationFailureReason {
| Self::FeeRateTooLow => true,
Self::CounterpartyAborted { .. }
| Self::NegotiationError { .. }
| Self::LocallyAbandoned
| Self::LocallyCanceled
| Self::ChannelClosing
| Self::CannotInitiateRbf => false,
}
Expand All@@ -188,7 +190,7 @@ impl core::fmt::Display for NegotiationFailureReason {
},
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::LocallyCanceled => f.write_str("splice locally canceled"),

Self::ChannelClosing => f.write_str("channel is closing"),
Self::FeeRateTooLow => f.write_str("feerate too low for RBF"),
Expand All@@ -207,7 +209,7 @@ impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason,
(1, msg, required),
},
(9, ContributionInvalid) => {},
(11, LocallyAbandoned) => {},
(11, LocallyCanceled) => {},
(13, ChannelClosing) => {},
(15, FeeRateTooLow) => {},
(17, CannotInitiateRbf) => {},
Expand DownExpand Up@@ -1955,7 +1957,7 @@ pub enum Event {
invoice_request: InvoiceRequest,
},
/// Indicates that a channel funding transaction constructed interactively is ready to be
/// signed. This event will only be triggered if at least one input was contributed.
/// signed. This event will only be triggered if a contribution was made to the transaction.
///
/// The transaction contains all inputs and outputs provided by both parties including the
/// channel's funding output and a change output if applicable.
Expand All@@ -1966,8 +1968,9 @@ pub enum Event {
/// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and
/// hence possible loss of funds.
///
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed
/// funding transaction.
/// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially)
/// signed funding transaction. For splices where you contributed inputs or outputs, call
/// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed.
///
/// Generated in [`ChannelManager`] message handling.
///
Expand All@@ -1976,6 +1979,7 @@ pub enum Event {
/// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts.
///
/// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager
/// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed
/// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed
FundingTransactionReadyForSigning {
/// The `channel_id` of the channel which you'll need to pass back into
Expand Down
127 changes: 96 additions & 31 deletions lightning/src/ln/channel.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -1724,7 +1724,7 @@ where
if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) {
chan.context.channel_state.clear_local_stfu_sent();
chan.context.channel_state.clear_remote_stfu_sent();
if chan.should_reset_pending_splice_state(false) {
if chan.should_reset_pending_splice_state(true) {
// If there was a pending splice negotiation that failed due to disconnecting, we
// also take the opportunity to clean up our state.
let splice_funding_failed = chan.reset_pending_splice_state();
Expand DownExpand Up@@ -1841,7 +1841,7 @@ where
None
},
ChannelPhase::Funded(funded_channel) => {
if funded_channel.should_reset_pending_splice_state(false) {
if funded_channel.should_reset_pending_splice_state(true) {
funded_channel.reset_pending_splice_state()
} else {
debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures");
Expand DownExpand Up@@ -2024,7 +2024,7 @@ where
"Received tx_abort while awaiting tx_signatures exchange".to_owned(),
));
}
if funded_channel.should_reset_pending_splice_state(true) {
if funded_channel.should_reset_pending_splice_state(false) {
let has_funding_negotiation = funded_channel
.pending_splice
.as_ref()
Expand DownExpand Up@@ -7159,7 +7159,7 @@ where

fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> {
if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) {
if self.should_reset_pending_splice_state(false) {
if self.should_reset_pending_splice_state(true) {
self.reset_pending_splice_state()
} else {
self.abandon_quiescent_action()
Expand DownExpand Up@@ -7216,7 +7216,7 @@ where

/// Returns a boolean indicating whether we should reset the splice's
/// [`PendingFunding::funding_negotiation`].
fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool {
fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool {
self.pending_splice
.as_ref()
.map(|pending_splice| {
Expand All@@ -7228,16 +7228,18 @@ where
funding_negotiation,
FundingNegotiation::AwaitingSignatures { .. }
);
if counterparty_aborted {
if allow_resumption {
// If we want to resume the negotiation after reconnecting, we must be
// in [`FundingNegotiation::AwaitingSignatures`] to not reset our state.
!is_awaiting_signatures
} else {
!is_awaiting_signatures
|| !self
.context()
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_received_commitment_signed()
} else {
!is_awaiting_signatures
}
})
.unwrap_or_else(|| {
Expand All@@ -7251,7 +7253,7 @@ where
}

fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> {
debug_assert!(self.should_reset_pending_splice_state(true));
debug_assert!(self.should_reset_pending_splice_state(false));

// Only clear the signing session if the current round is mid-signing. When an earlier
// round completed signing and a later RBF round is in AwaitingAck or
Expand DownExpand Up@@ -7325,7 +7327,7 @@ where
}

pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> {
if !self.should_reset_pending_splice_state(false) {
if !self.should_reset_pending_splice_state(true) {
return None;
}

Expand DownExpand Up@@ -12779,30 +12781,93 @@ where
}
}

#[cfg(test)]
pub fn abandon_splice(
&mut self,
) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> {
if self.should_reset_pending_splice_state(false) {
let tx_abort =
msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() };
let splice_funding_failed = self.reset_pending_splice_state();
Ok((tx_abort, splice_funding_failed))
} else if self.has_pending_splice_awaiting_signatures() {
Err(APIError::APIMisuseError {
pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> {
if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) {
let splice_funding_failed = self.abandon_quiescent_action();
debug_assert!(splice_funding_failed.is_some());
let str = "Manually canceled funding contribution";
let err = if self.context.channel_state.is_local_stfu_sent()
&& !self.context.channel_state.is_remote_stfu_sent()
{
// If we've already sent `stfu` and haven't received the counterparty's yet, we know
// it corresponds to our action.
ChannelError::WarnAndDisconnect(str.into())
} else {
// We don't need to send `tx_abort` because our action still pending means we're not
// quiescent for it.
ChannelError::Ignore(str.into())
Comment thread
wpaulino marked this conversation as resolved.
};
return Ok(InteractiveTxMsgError { err, splice_funding_failed });
}

let funding_negotiation = self
.pending_splice
.as_ref()
.and_then(|pending_splice| pending_splice.funding_negotiation.as_ref());
let Some(funding_negotiation) = funding_negotiation else {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; already awaiting signatures",
self.context.channel_id(),
"Channel {} does not have a pending splice negotiation",
self.context.channel_id()
),
})
Comment thread
wpaulino marked this conversation as resolved.
} else {
Err(APIError::APIMisuseError {
});
};

let made_contribution = match funding_negotiation {
FundingNegotiation::AwaitingAck { context, .. } => {
context.contributed_inputs().next().is_some()
|| context.contributed_outputs().next().is_some()
},
FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => {
interactive_tx_constructor.contributed_inputs().next().is_some()
|| interactive_tx_constructor.contributed_outputs().next().is_some()
},
Comment thread
wpaulino marked this conversation as resolved.
FundingNegotiation::AwaitingSignatures { .. } => self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_local_contribution(),
Comment thread
wpaulino marked this conversation as resolved.
};
if !made_contribution {
return Err(APIError::APIMisuseError {
err: format!(
"Channel {} splice cannot be abandoned; no pending splice",
self.context.channel_id(),
"Channel {} has a pending splice negotiation with no contribution made",
self.context.channel_id()
),
})
});
}

// We typically don't reset the pending funding negotiation when we're in
// [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on
// re-establishment, so we still need to handle this case separately if the user wishes to
// cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can
// guarantee to never have sent any signatures to the counterparty, or have processed any
// signatures from them.
if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) {
let already_signed = self
.context
.interactive_tx_signing_session
.as_ref()
.expect("We have a pending splice awaiting signatures")
.has_holder_tx_signatures();
if already_signed {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this suffice? We call reset_pending_splice_state which asserts that !has_received_commitment_signed() but we don't check it here?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only process the counterparty's initial commitment_signed once the user calls back with ChannelManager::funding_transaction_signed.

return Err(APIError::APIMisuseError {
err: format!(
"Channel {} has pending splice negotiation that was already signed",
self.context.channel_id(),
),
});
}
}

debug_assert!(self.context.channel_state.is_quiescent());
let splice_funding_failed = self.reset_pending_splice_state();
Comment thread
wpaulino marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about updating PendingFunding::contributions when reseting? This may be a pre-existing issue, though.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already fixed in a547960.

debug_assert!(splice_funding_failed.is_some());
Ok(InteractiveTxMsgError {
err: ChannelError::Abort(AbortReason::ManualIntervention),
splice_funding_failed,
})
}

/// Checks during handling splice_init
Expand DownExpand Up@@ -15584,7 +15649,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
ChannelState::ChannelReady(_) => {
channel_state.clear_local_stfu_sent();
channel_state.clear_remote_stfu_sent();
if self.should_reset_pending_splice_state(false)
if self.should_reset_pending_splice_state(true)
|| !self.has_pending_splice_awaiting_signatures()
{
// We shouldn't be quiescent anymore upon reconnecting if:
Expand DownExpand Up@@ -15974,7 +16039,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> {
// We don't have to worry about resetting the pending `FundingNegotiation` because we
// can only read `FundingNegotiation::AwaitingSignatures` variants anyway.
let pending_splice =
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false));
self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(true));

let monitor_pending_tx_signatures =
self.context.monitor_pending_tx_signatures.then_some(());
Expand Down
Loading
Loading